Handling runtime failures

Do you also have such days when lots of things seem to go wrong? Computers have that too. They want to read from a file, but it has vanished. Or write to a network connection but the network is down. Computer programs must cope with these kinds of errors, because normally the program shouldn’t crash once such an error occurs.

Mainstream object-oriented languages like Java and C# support native exception handling to make this easier. Although it is often very convenient, sometimes we can do better. In this post, I’ll show a few ways of handling exceptional situations.

Read more: Handling runtime failures

I’m sure you can think of a lot of cases where problems can occur at runtime. For starters, every interaction with the outside world may fail. For example, when you read a file, the data might turn out to be corrupted. When reading from a network stream, the connection may break. So all kinds of unexpected things may happen.

When there were no exceptions

When programming in C, no basically exception handling is available. (There is some workaround using setjmp and longjmp, but people advise against using this because of some bad side effects.) This leaves the developer basically with two options:

  • Functions set a global variable with an error value, which clients need to check regularly.
  • Make all functions return an error code which client code must check.

Both options have big issues. The first solution has the advantage that return values can be actual values: you can declare double sqrt(double from) instead of int sqrt(double from, double* result), but you basically lose all concurrency. Another problem is: who is going to clear the error variable? If you forget in your error handling routine, then the next call is considered a failure as well. Using global variables is almost always a bad idea, even though it seems promising at first.

The second solution has the issue that it forces consuming code to wrap all calls in an if..else construct. This makes for cumbersome programming. And forgetting to check the return value is terribly easy.

But still, the errors won’t go away. So developers require a way of error handling that doesn’t get in the way.

Unchecked exceptions

That is why programming language designers introduce exception handling. The idea is to execute a bunch of statements, but when one of those fails, jump to a specific exception handling routine. So take for example this snippet:

try {
    string[] lines = File.ReadAllLines(path);
    Array.Sort(lines);
    File.WriteAllLines(path);
} catch(DirectoryNotFoundException d) {
    // handle the case that the path to the file is invalid
}catch(IOException e) {
    // file could not be accessed
} 

In this snippet, the ReadAllLines and WriteAllLines methods may throw a number of exceptions for a number of error situations. If any of these methods fails with an IOException or DirectoryNotFoundException, then the corresponding error handling part is run. So exception handling in this way makes it possible to introduce routines to handle specific error cases.

Exception handling this way looks much better than the error handling in C, because as a developer, you can focus more on the normal program flow.

Handling checked exceptions

There is one drawback however: you still can forget to handle exceptions. Exceptions are not part of a method’s signature, so you are not required to handle them. If you don’t, your program will ultimately crash. But shouldn’t it be better to be notified in advance that something can go wrong by, say, the compiler? I don’t know for sure, but that might be the idea of the Java designers to make checked exceptions part of the method signature:

public void SortFile(String path) {
    try {
        String[] lines = readLines(path);
        Arrays.sort(lines);
        writeLines(lines, path);
    } catch(IOException e) {
        // file could not be accessed
    }
}

private static String[] readLines(String path) throws IOException { ... }
private static String[] writeLines(String[] lines, String path) throws IOException { ... }

If a method declares to throw exceptions, like the readLines and writeLines, then the calling method must either catch this specific exception type in a try..catch block, or declare the method to throw an IOException itself. So catching the exception is checked at compile time.

In practice, this works well often enough, but it might get in the way sometimes.

public interface Command {
    void execute();
}

public class DatabaseCommand {
    public void execute() {
        executeQuery(...);
    }
}

The above seems to work, right? Except if executeQuery declares to throw an exception. Then there are two options. The first one is to let DatabaseCommand handle the exception itself. However, that might not be very well possible, because it might depend on the context in which the command is run (is it scripted, or was it called from a UI event?)

The second option is to forward the exception by declaring the exception on execute(). But that means that the exception also must declared on Command.execute(), so therefore all consumer code must catch the exception, even when they call execute() on a different Command implementation.

ResultType

What if we wrap the return value in an object and let the consumer check? For instance, we can create a Result type like this:

public class Result
{
    public static Result<T> Success<T>(T value)
        => new Result<T>
        {
            IsSuccess = true,
            Value = value
        };

    public static Result<T> Error<T>(Exception e)
        => new Result<T>
        {
            IsSuccess = false,
            Error = e
        };
}

public static class Result<T>
{
    public bool IsSuccess { get; init; }
    public T? Value { get; init; }
    public Exception? Error { get; init; }
}

public sealed class Unit
{
    public static readonly Unit instance = new();
    private Unit() { }
}

This just holds a possible result value or a possible error, along with a variable to distinguish which of the two is valid. I move the Success and Error methods to a separate non-generic class to make them somewhat easier to use. Usage is as follows:

public class Program
{
    public static int Main(string[] args)
    {
        Result<string> fileName = ParseFileArg(args);
        if (fileName.IsSuccess)
        {
            Result<string[]> contents = ReadFile(fileName.Value);
            if (contents.IsSuccess)
            {
                Array.Sort(contents.Value);
                Result<Unit> done = WriteFile(contents.Value, fileName.Value);
                if (done.IsSuccess)
                    return 0;
                return -1;
            } 
            else 
                return -2;
        }
        else
            return -3;
    }

    private static Result<string> ParseFileArg(string[] args)
    {
        if (File.Exists(args[0]))
            return Result.Success(args[0]);
        return Result.Error<string>(new FileNotFoundException());
    }

    private static Result<string[]> ReadFile(string file)
    {
        try
        {
            var value = File.ReadAllLines(file);
            return Result.Success(value);
        } 
        catch(Exception e)
        {
            return Result.Error<string[]>(e);
        }
    }

    private static Result<Unit> WriteFile(string[] contents, string file)
    {
        try
        {
            File.WriteAllLines(file, contents);
            return Result.Success(Unit.instance);
        } 
        catch(Exception e)
        {
            return Result.Error<Unit>(e);
        }
    }
}

The ReadFile and WriteFile wrap existing exceptions into Result instances. ParseFileArg creates its own exception to indicate an error. Note that the error type can be anything; I chose to use Exception but you can also have an enum, list of strings or whatever fits in your situation.

I also introduced a Unit type so that I don’t have to provide a separate implementation to handle the void case. Unit is basically a wrapper for void, so that a method that returns nothing can be used as a Func.

Now I don’t know what you’d think about the Main implementation, but I find it awful. Compared to the exception handling, this is much more complex. It is full of if..else constructs just as it would be when written in C, except now it has an additional indirection so it is even more complex.

It is not an accident that every Result return value has an accompanying if..else. Maybe we can move the check to Result? We can, if we change Result<T> like this:

public sealed class Result<T>
{
    public bool IsSuccess { get; init; }
    public T? Value { get; init; }
    public Exception? Error { get; init; }

    public Result<TU> Select<TU>(Func<T, TU> ifSuccess)
        => IsSuccess
            ? Result.Success(ifSuccess(Value!))
            : Result.Error<TU>(Error!);
}

The added Select method gives Result<T> the power to create another Result<U> based on the current IsSuccess state and the result of ifSuccess. In case the current Result<T> holds an error, then the current error is just propagated without evaluating ifSuccess at all. This is necessary to allow the construct to work just like a try..catch.

Now we moved the condition check to Result<T>, we can update the Main method as follows:

 public static int Main(string[] args)
{
    Result<string> fileName = ParseFileArg(args);
    Result<string[]> contents = fileName.Select(ReadFile);
    Result<Unit> sorted = contents.Select(Sort);
    Result<Unit> done = contents.Select(value => WriteFile(value, fileName.Value!));
    return done.IsSuccess ? 0 : -1;
}

In order to make all statements in Main act on the same abstraction level, I introduced a Sort method. This does nothing but sort the array and wrap the result in a Result<Unit>. Assuming that Array.Sort never fails, it can return a Result<Unit> that models a success result:

private static Result<Unit> Sort(string[] array)
{
    Array.Sort(array);
    return Result.Success(Unit.Instance);
}

Error handling is now baked into the design. This makes it impossible to forget, but also easy to do. It is also fewer lines than the try..catch equivalent. However, the implementation is still a more cumbersome because we have Result definitions in every single line that does actual work. We can get rid of that by applying the LINQ query language of C#. With that, the Main method will look as follows:

 public static int Main(string[] args)
{
    Result<Unit> done =
        from path in ParseFileArg(args)
        from contents in ReadFile(path)
        from unit1 in Sort(contents)
        from unit in WriteFile(contents, path)
        select unit;
    return done.IsSuccess ? 0 : -1;
}

Only the result of the query is of type Result<Unit>, the variables path, contents, unit1 and unit in the query are of the inner types (string, string[], Unit and Unit).

Since the Linq language is basically a wrapper around ordinary C# methods, we still have to provide those methods. For our Result<T> type, we need to have a Select and a SelectMany method. Select can stay unmodified from what we had, we now only have to provide SelectMany. The C# compiler uses SelectMany to translate the from expressions into new Result<T> instances. We can provide the following implementation:

public Result<TU> SelectMany<TC, TU>(Func<T, Result<TC>> collector, Func<T, TC, TU> selector)
    => IsSuccess
        ? collector(Value!)
                .Select(v => selector(Value!, v))
        : Result.Error<TU>(Error!);

This will make the Result<T> class look as follows:

public sealed class Result<T>
{
    public bool IsSuccess { get; init; }
    public T? Value { get; init; }
    public Exception? Error { get; init; }

    public Result<TU> Select<TU>(Func<T, TU> ifSuccess)
        => IsSuccess
            ? Result.Success(ifSuccess(Value!))
            : Result.Error<TU>(Error!);

    public Result<TU> SelectMany<TC, TU>(Func<T, Result<TC>> collector, Func<T, TC, TU> selector)
        => IsSuccess
            ? collector(Value!)
                    .Select(v => selector(Value!, v))
            : Result.Error<TU>(Error!);
}

Leave a Reply

Your email address will not be published. Required fields are marked *