Dwarves
Memo
Type ESC to close search bar

Error Handling Patterns

Error handling is one of the most critical aspects of software development, as it ensures that applications behave correctly even in the presence of unexpected inputs or conditions. Over the years, many error-handling patterns have evolved in different programming languages.

1. Return Codes/Status Codes

This is one of the most basic forms of error handling and is common in older, low-level programming languages such as C. A function returns a value that indicates whether it succeeded or failed. For example, it might return 0 for success or -1for failure. The caller is responsible for checking the return value and handling any errors.

Always check the return values when using this pattern. Missing a check can easily lead to silent bugs that are hard to trace.

Example:

int divide(int a, int b, int *result) {
    if (b == 0) {
        return -1;  // error: division by zero
    }
    *result = a / b;
    return 0;  // success
}

int main() {
    int result;
    if (divide(10, 0, &result) != 0) {
        printf("Error: Division by zero!\n");
    }
}

Pros:

Cons:

2. Exceptions (Try-Catch)

Exceptions are a more modern and structured way of handling errors, used in languages like Python, Java, and C#. When an error occurs, the program “throws” an exception, which can be caught and handled using a try-catch block. This separates normal flow from error-handling logic.

Don’t overuse exceptions for flow control, and never swallow exceptions without logging or handling them properly. Always catch specific exceptions rather than using generic ones like Exception in Python or Throwable in Java.

Example:

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Error occurred: {e}")

Pros:

Cons:

3. Error Objects or Results

This pattern forces the function to return an object that explicitly represents either a successful result or an error. It is commonly used in functional programming languages like Rust, Haskell, and also in Swift. In Rust, for example, the Result type can be Ok for success or Err for failure.

Embrace this pattern when available. It forces you to deal with both success and error casesexplicitly, reducing the likelihood of missed error handling.

Example (Rust):

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        return Err("Division by zero".to_string());
    }
    Ok(a / b)
}

fn main() {
    match divide(10, 0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

Pros:

Cons:

4. Assertions

Assertions are a debugging tool that checks if certain conditions hold true. If the assertion fails, the program crashes, usually with a helpful error message. This pattern is mainly used for development and debugging, not for production error handling.

Don’t use assertions for regular error handling. They are meant for development and debugging purposes, not for catching user-facing errors in production.

Example (Python):

def divide(a, b):
    assert b != 0, "Division by zero!"
    return a / b

divide(10, 0)  # This will raise an AssertionError

Pros:

Cons:

5. Callbacks (Error-First)

In environments that deal with asynchronous operations, like Node.js, error-first callbacks are a common pattern. The first parameter of the callback is an error (if any), and the second is the result.

When using callbacks, always check the error argument first. Don’t forget to handle errors properly in every callback.

Example (JavaScript):

function divide(a, b, callback) {
    if (b === 0) {
        return callback(new Error("Division by zero"), null);
    }
    callback(null, a / b);
}

divide(10, 0, (err, result) => {
    if (err) {
        console.error(err.message);
    } else {
        console.log(result);
    }
});

Pros:

Cons:

6. Promise-Based Error Handling

Promises are an evolution of callbacks, mainly used in asynchronous programming (e.g., JavaScript). They allow for cleaner handling of asynchronous operations, using .then() for success and .catch() for errors.

Use Promises to make your asynchronous code more readable. Pay attention to the promise chain, and always handle .catch() for potential errors.

Example (JavaScript):

function divide(a, b) {
    return new Promise((resolve, reject) => {
        if (b === 0) reject(new Error("Division by zero"));
        else resolve(a / b);
    });
}

divide(10, 0)
    .then(result => console.log(result))
    .catch(error => console.error(error.message));

Pros:

Cons:

7. Pattern Matching

Pattern matching is used in functional languages like Haskell, Rust, and Scala to handle different outcomes of a computation. This allows developers to decompose data structures and handle each case explicitly.

Pattern matching is powerful, but be sure to handle all possible cases. If you miss one, your program might crash or behave unexpectedly.

Example (Rust):

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err("Division by zero".to_string())
    } else {
        Ok(a / b)
    }
}

fn main() {
    match divide(10, 0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

Pros:

Cons:

8. Panic (Crash)

Some languages like Rust and Go use panics for non-recoverable errors. A panic results in the program crashing. In Rust, panics can be caught, but in general, this pattern is reserved for situations where the program can’t reasonably continue.

Use panics sparingly. They should only be used for truly exceptional, unrecoverable errors, not for ordinary cases like bad user input.

Example (Go):

package main

import "fmt"

func divide(a, b int) int {
    if b == 0 {
        panic("Division by zero!")
    }
    return a / b
}

func main() {
    fmt.Println(divide(10, 0))
}

Pros:

Cons: