Skip to main content
Back to blog

Software development

How to handle and reduce risk of exceptions

Samuel Beausoleil
Mar 11, 2021 ∙ 5 mins
woman standing with a colorful background

Here is the second instalment in our series of how to repair trust in numeric solutions. If you haven’t yet read our first blog, we recommend you to take a look at How to avoid failure in your digital projects.

All software has areas where it is easy to predict potential issues. Be it while searching in a list or reading a file, issues can happen and it is important to handle them. In handling the errors and exceptions correctly (hereby “exceptions”), we can make our software more resilient and increase our odds of recovering from an exception.

Handling exceptions

Try / Catch [/ Finally]

handling exceptions, the try/catch structure
Relevant XKCD: https://xkcd.com/1188/

The try/catch structure introduced by COBOL in the 60s was one of the first native ways for a language to handle exceptions. Even today, it is the most commonly used method. It’s amazing at catching seldom occurring exceptions, and regrouping actions to take in case of an issue. However, it isn’t without issue. It covers large areas of code and not just the critical point, which discourages recovery of an error at its source, and thus reduces the silent resilience of the code. Since the catch clause is so often far from the cause, more often than not what simply happens is logging of the exception and throwing of a runtime exception or any other form of early exit from the function. Even if it is handled and recovered from, since it covers an area instead of a specific operation, it is difficult to go back to right after the vexatious operation.

Furthermore, the non-linear flow of programs using exceptions with try/catch gain greatly in complexity, especially when exceptions must bubble several levels up before being handled.

Result

The Result is the direct answer to the problems of the try/catch. A Result is a return type for methods where we can predict a reasonable risk of failure and where it should be logical to expect some kind of recovery to happen

fn read_username_from_file() -> Result<String, io::Error> {
    let file: Result<File, io::Error> = File::open("user.txt");

    let mut file = match file {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut buffer = String::new();
    match file.read_to_string(&mut buffer) {
        Ok(_) => Ok(buffer),
        Err(e) => Err(e),
    }
}

fn main() {
    let username = match read_username_from_file() {
        Ok(name) => name,
        Err(_) => {
            println!("Couldn't read username file.");
            read_input("Please enter your username:")
        }
    };

    println!("Your username is {}.", username);
}

As demonstrated in the previous Rust example, the usage of Results allows us to handle exceptions in an elegant, clear, and predictable manner. We can predict that file X can’t be read for multiple reasons. It is wise then to plan immediately and clearly what to do if something bad happens. In this case, asking the user to manually enter their username.

Obviously, Result is not a silver bullet for all exceptions, but it is a good start. Traditional try/catch structures are well suited for handling more generic use cases.

Lessening the odds of common exceptions

Finally, it is always preferable to reduce risk where possible. Result is a step in the right direction, but it is not all. Many ways of programming increase the risk of exceptions. In this section, I’ll detail a few elements that do just that, and how to avoid them.

Null

The infamous null is a great enemy of programmers who are not used to its usage and that now must face it. Even for those who are used to it, it can cause some issues when its appearance isn’t predicted and accounted for. Its inventor, Tony Hoare, has himself later stated that his invention of null pointers was his “Billion dollar mistake.”

That is why there are more and more methods that are encouraged to offer alternatives to its usage.

  • In languages that support neither overloading, nor default values for optional parameters, use the type Option as a parameter. This way, you communicate that a parameter is optional, and you are reminded to handle the scenario where that argument is empty every time you work on that function.
  • Similarly, it is possible that your function does not return anything in some scenarios. In such a case, always return Option. This way, those who read your code will never be surprised by the occurrence of a null value, and will be reminded to check the returned value to see if it is empty before doing anything with it.
  • When a collection is to be returned but there is nothing to actually return, instead of surprising your fellow programmer with a null, simply return an empty collection. All collection implementations support being empty when methods are called on them, which will allow the program to continue naturally.

Invalid memory

Errors of read/write on invalid pointers is another common source of issues in languages that support raw pointers. They are caused by trying to access a variable that has already been freed. An important concept to use then is RAII. By avoiding the use of raw heap pointers in general execution code paths and by tying such pointers to the lifetime of other objects, we ensure that there never again are memory access errors again in a seamless way. This way, since the lifetime of the heap pointer is linked to the lifetime of a stack object, we are assured that the pointer is freed when it’s accessor (the stack object) is deleted naturally by the ending of its stack frame.

Type your variables

By indicating the type of your variables, you reduce the risk of mistakes and reduce the risk of another programmer in your team calling your function with the wrong data type. These problems this way will be caught at compile time.

Test

An exhaustive test suite with excellent code coverage reduces the risk of bugs going undetected or that regressions appear. Do not content yourself with the usual unit test, integration tests are also useful to check the bridges between the different parts of your program.

Closing words

As programmers, it is our responsibility to produce programs that are both exact and reliable. Too often, we have valued and taken care of only the exactitude of it. It is time we start paying heed to reliability, and starting doing our job completely.

Photo credit: ThisisEngineering RAEng