DEV Community

Cover image for Exception handling is an Antipattern | Avoid when possible
ziaulhasanhamim
ziaulhasanhamim

Posted on • Originally published at hamim.hashnode.dev

Exception handling is an Antipattern | Avoid when possible

Some Background

In OOP languages like C++/C#/Java/Python exceptions were introduced to handle the exceptional situation. Exception handling is common and every introductory course covers it. But we should rethink its usage. Is it really beneficial to use? Let's find out.

The Problem of Exceptions

I think most people know about goto statements and why they are considered bad. But if we think properly, are exceptions different from gotos? Probably not. The problem with both is they break natural code flow. So exceptions are really a modern implementation of gotos.

Exceptions should only be used when something horribly gone wrong and the application has to terminate(Use them more like a panic in other languages).

The biggest problem is it breaks the flow of code. For me, the definition of function/method is a processing mechanism that takes in input and returns the processed output(Sometimes there can be some side effects but better to avoid). Exceptions break that definition. Exceptions really make methods unpredictable. You can't say how the method call will behave if the method throws. Once an exception is thrown It breaks every method call until it gets caught.

*Exceptions tend to allow, even encourage, programmers to ignore the possibility of an error, assuming it will be magically handled by some earlier exception handler. The exception never forces a programmer to do something with it instead of just ignoring it.
*

JsonNode ReadJsonFromFile(string path)
{
    if (FileHelper.IsLocked(path))
    {
        throw new Exception("File is locked");
    }
    // Nice path
}
void AddMovieFromJson()
{
    var moviesJson = ReadJsonFromFile("~/documents/movies.txt"); // Suppose this file is locked and throws an exception
    db.AddMovies(ParseMovies(moviesJson));
}
Enter fullscreen mode Exit fullscreen mode

Although ReadJsonFromFile("~/documents/movies.txt") throws an exception but the compiler doesn't force you to handle that. Most programmers will ignore this kind of exceptions because the code compiles fine without any warning or error.

Exceptions secretly break consumer code

Suppose you have a method that will create a user on the database.

User CreateUser(UserCreateReq req)
{
    var user = db.AddUser(req);
    return user;
}
Enter fullscreen mode Exit fullscreen mode

Someday you felt the need of adding some email validation to the user.

User CreateUser(UserCreateReq req)
{
    if (!IsEmailValid(req.Email))
    {
        throw new ValidationException("User email is invalid");
    }
    var user = db.AddUser(req);
    return user;
}
Enter fullscreen mode Exit fullscreen mode

Now the consumer applications of this method can literally break on runtime. This is a breaking change but it doesn't appear to be a breaking change because the definition of the method is really the same. The definition still promises to return a User object as before. But now sometimes the method can break the promise of returning a User and instead throw an exception. That's why Exceptions are always unpredictable and a nasty way of breaking return promise

State Corruption

Consider an exception unexpectedly being thrown part way through modifying a large data structure. How likely is it that the programmer has written code to correctly catch that exception, undo or reverse the partial changes already made to the data structure, and re-throw the exception? Very unlikely! Far more likely is the case that the programmer simply never even considered the possibility of an exception happening in the first place because exceptions are hidden, not indicated in the code at all. When an exception then occurs, it causes a completely unexpected control transfer to an earlier point in the program, where it is caught, handled, and execution proceeds – with a now corrupt, half-modified data structure!

Here we go again: Concurrency and async-await

Exception handling more-or-less inherently implies that there is a sequential call chain and a way to "go back" through the callers to find the nearest enclosing catch block. This is horribly at odds with any model of parallel programming, which makes exception handling very much less than ideal going forward into the many-core, parallel programming era which is the future of computing.

Even when considering the simplest possible concurrent programming model of all – some threads processing all of the elements of an array in parallel – the problem is immediately obvious. What should you do if you have 5 threads and just one of them throws an exception? Complicated Right?

Async Await

The situation is not so different with TPL and async-await. Like concurrency model in C# would be way easier to implement if there were no exceptions. Async with exceptions is a nightmare for beginner programmers, like you shouldn't do continuewith because exceptions get ignored, always await else exceptions get ignored, and sometimes stack trace gets messy and hard to understand whereas simple input-output behavior of methods makes concurrency a piece of cake. If you want your applications to have frictionless concurrency switch to pure functions and input-output without side effects.

Performence

Exceptions can decrease performance drastically(I mean horribly). Even a single exception per request in an ASP.NET Core application can decrease requests per second exponentially. So you can imagine if you have a good amount of exceptions thrown in your application how big of an impact it can bear on the performance.

To Conclude

I really think if C# was built in this decade It wouldn't even have the feature of exceptions. I know it's a bold statement to make but it's almost certain.

The solution

So exceptions shouldn't be used? So how should we go about returning errors? Luckily there is a great way to handle errors. It is used by so many languages nowadays like Rust, F#, Scala, Go(Go uses tuple as union) etc. It is done using a monad. Don't worry you don't need to know about monads to use this. Generally, It is accomplished using a Union type.

union Result = either SuccessResult or Error;
Enter fullscreen mode Exit fullscreen mode

This result union can contain either an actual success return value or an error.

There are many libraries in C# that try to achieve this behavior Such as ErrorOr, OneOf, BetterErrors, AnyOf etc. I'm gonna use BetterErrors in this example.

First add the package dotnet add package BetterErrors.

Then add the using at the top of your file

using BetterErrors;
Enter fullscreen mode Exit fullscreen mode

Let's write a method to get users from DB.

Result<User> GetUser(Guid id, Database db)
{
    if (!db.UserExists(id))
    {
        return new NotFoundError("User id not found");
    }

    return db.FindUser(id);
}
Enter fullscreen mode Exit fullscreen mode

Notice here we are using Result<User> as return type instead of User and returning NotFoundError instead of throwing an exception. So there is no side-effect of this method. NotFoundError is provided by BetterErrrors library. You can create your own error types by extending the Error type. Notice there is an implicit conversion from User to Result<User> and NotFoundError to Result<User>.

Now let's see how to consume the Result.

GetUser(id).Switch(
    user => Console.WriteLine(user.Username),
    error => Console.WriteLine(error.Message)); 
Enter fullscreen mode Exit fullscreen mode

The switch will call the first delegate with the returned user if it's a successful result else it will call the second delegate. You can use the Match method to return something from those delegates.

string message = GetUser(id).Match(
    user => $"Username is {user.Username}",
    error => $"error: {error.Message}")); 
Enter fullscreen mode Exit fullscreen mode

You can do the same with the if else statement.

var userResult = GetUser(id);
if (userResult.IsSuccess)
{
    Console.WriteLine(userResult.Value.Username);
    return;
}
Console.WriteLine(userResult.Error.Message);
Enter fullscreen mode Exit fullscreen mode

You could also transform the Result<User> to Result<AnotherType> with Map method.

Result<Profile> message = GetUser(id).Map(user => db.GetProfile(user)); 
Enter fullscreen mode Exit fullscreen mode

The delegate passed into Map method will only be called if Result<User> contains a success result else it will create Result<Profile> from the error of Result<User>.

Head to full documentation of BetterErrors to know more.

Usage in real world

Now let's see how you can use this approach in an ASP.NET Core application.

Here is an endpoint to create a post.

app.MapPost("/api/posts/", (CreatePostRequest req, IPostsService postsService) =>
{
    return postsService.CreatePost(req)
        .Match(
            post => Results.Created($"/posts/{post.Id}", post),
            err => Results.BadRequest(err)
        );
});
Enter fullscreen mode Exit fullscreen mode

postsService.CreatePost returns a Result of Post. So we are matching on that result to return either a Created response or a BadRequest response.

Here is a kind of implementation of IPostsService

public sealed class PostsService : IPostsService
{
    private readonly AppDbContext _dbContext;

    public PostsService(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    public Result<Post> CreatePost(CreatePostRequest req)
    {
        if (!_db.Posts.Any(post => post.Title == req.Title))
        {
            var errorInfos = new FieldErrorInfo[]
            {
                new(nameof(post.Title), "Post title is duplicate")
            };
            return new ValidationError("Provided data is invalid", errorInfos);
        }
        var post = new Post()
        {
            Title = req.Title,
            Content = req.Title
        };
        _dbContext.Add(post);
        _dbContext.SaveChanges();
        return post;
    }
}
Enter fullscreen mode Exit fullscreen mode

This is a simplified version but your code will more or less look like this.

Benefits of this approach

So you can already see how easy it is to handle errors with this approach. Everything is predictable. Functions don't have side effects. And you are forced to check If there is an Error.

Top comments (12)

Collapse
 
cicirello profile image
Vincent A. Cicirello

Exception handling is definitely not an antipattern. You are confusing weak use by beginners who lack a thorough understanding of when and how to properly use it.

Also your example where you add code to throw an exception and indicate that it doesn't appear to be a breaking change is missing something critical. Specifically, the method in that example isn't documented, so the contract for that method hasn't been specified. Throwing an exception in a method that didn't previously throw an exception changes the contract of the method, and is a breaking change. The docs for the method must fully specify its contract. So changing it to throw an exception changes the contract requiring changing the docs, revealing the breaking change.

Collapse
 
ziaulhasanhamim profile image
ziaulhasanhamim • Edited

I think you are lacking on the definition of antipattern. Antipattern is not something that you can't write good code with. Antipattern is something that makes it harder to write good code. You pointed out that often beginers makes the mistake about exceptions. But why? Because they are hard use properly. I've seen many code bases where exceptions are just ignored assuming it won't happen or handled by an earlier method.

Specifically, the method in that example isn't documented, so the contract for that method hasn't been specified

Really? Okay I give you two option for implementation of a method. First method does exactly what it said. It doesn't require any extra documentation to explain itself. It doesn't have any side effect. The behaviour is predictable. So you can look at the definition and say okay I know exactly what it does. What a charm! Second one can throw exceptions and have side effect. Which one is better? Self explanatory predictable or one with documentation and unpredictable behaviour.

The docs for the method must fully specify its contract. So changing it to throw an exception changes the contract requiring changing the docs, revealing the breaking change.
There can be better way where you don't even need to look at docs to understand it's breaking change. You will get an compilation error. My experience tells me compilion errors are always better than runtime errors or maybe bugs. If you can write code that explain itself is much better than code that requires extra documentation.

Also there were other points like data corruption, concurrency. Like I know that these can be avoided. But exceptions can create those problems. And they are not rare. You can easily find misuse of exception handling.

That's why it's rare to see exception as a feature in modern languages unless they are successor to any old language, and they have to maintain compatibility. Now it's well understood that simple input-output format functions/methods are best for any use case. Natural flow of code is important

Collapse
 
cicirello profile image
Vincent A. Cicirello

The reason a beginner might misuse exception handling is the same for anything else they may misuse. They are new and early in learning process. Exception handling doesn't increase that. By your logic everything is an antipattern.

The alternative of returning result along with status/error code has all the same issues that you see as a disadvantage to exceptions.

Thread Thread
 
ziaulhasanhamim profile image
ziaulhasanhamim

No beginners do mistake about exceptions have pitfalls. As a said they have side effect whereas simple input-output mechanism always better. That's why it is adopted in new languages

Thread Thread
 
cicirello profile image
Vincent A. Cicirello

No. Claiming something is "always better" than something else is rarely a true statement. Claiming that a language feature is an antipattern independent of how it's used is a misunderstanding of the notion of a pattern/antipattern. And just because something is the case in some new languages doesn't make it "better". If a language is "new" it hasn't stood the test of time yet.

Thread Thread
 
ziaulhasanhamim profile image
ziaulhasanhamim

Again, you are lacking on the definition of antipattern. Antipattern doesn't mean a language feature is bad independent of how it's used. It means that there is a higher possibility that it can lead to bad code. I have explained my best that why exceptions can lead to bad behavior of code

Thread Thread
 
cicirello profile image
Vincent A. Cicirello

As one who teaches SE courses now and again, I absolutely do understand antipatterns. Your blanket declaration that exception handling is an antipattern is absolute nonsense. A language feature alone, without usage context, cannot be an antipattern.

There are antipatterns involving exception handling, such as the hiding exceptions antipattern, the raising unrelated/nonspecific exceptions antipattern, the catch then re-raise antipattern. There are also many exception handling related patterns.

But no, exception handling itself is neither an antipattern nor a pattern.

Thread Thread
 
ziaulhasanhamim profile image
ziaulhasanhamim

In your logic gotos are good to use too. Because they are also language feature. Same goes for pointer you should use that too. Don't forget goto, pointer everything provides a value. But if write C# code using that most people are gonna say avoid them. Same goes for exception they provide a value but still it's better to avoid. Not all language feature is good and shouldn't be used.

Thread Thread
 
cicirello profile image
Vincent A. Cicirello • Edited

Those are also neither patterns nor antipatterns. They are just parts of languages that you can either use or not use. It is how you use them that may be a pattern or may be an antipattern or may be neither.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

I'm sorry, I could not finish reading. There are so many things wrong with this I just stopped.

You are completely misunderstanding exceptions. Exceptions are the mechanism the language/application uses to inform us that something went wrong. Nobody, and I mean nobody has ever encouraged me to ignore the possibility of exceptions. The key word here is exception. It is an exceptional case, and it is therefore good and nice to be outside of the happy path.

Exceptions are also very helpful: Their main purpose is to alert, and alert they do indeed. They are alerting you to the possiblity that (taking from your example) a file is read-only or non-existing. But most importantly they are telling you that your application is not stable.

Maybe you get the idea that programmers are encouraged to ignore them because you see so many junior and mid developers adding try..catch blocks everywhere, sometimes with an empty catch block. Now that's the anti-pattern. Not the exceptions themselves.

Collapse
 
ziaulhasanhamim profile image
ziaulhasanhamim

I think you got my point wrong. I clearly state that exception handling is an antipattern not exception itself. Also, I mentioned that exceptions should only be used if something gone horribly wrong not just for control flow. They have all sorts of disadvantages that I pointed out. For these demerits it's rare to see exception as a feature in modern languages unless they are successor to any old language, and they have to maintain compatibility.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

While I get your clarification point, I cannot shake the feeling that if I agree with you, I'll be agreeing that exception handling must go away altogether. This is why I drove myself to commenting. Saying "exception handling is an anti-pattern" is too bold of a statement.