Exception handling is an Antipattern | Avoid when possible

Exception handling: The bad, and the ugly in C# and .NET

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));
}

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;
}

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;
}

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;

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;

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);
}

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));

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}"));

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);

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

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

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)
        );
});

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;
    }
}

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.