Skip to content

Instantly share code, notes, and snippets.

@Doy-lee
Last active February 11, 2024 11:04
Show Gist options
  • Save Doy-lee/0c4d23af4afc51ea8b03b60d0af9a1fb to your computer and use it in GitHub Desktop.
Save Doy-lee/0c4d23af4afc51ea8b03b60d0af9a1fb to your computer and use it in GitHub Desktop.
Error handling using a 'sink'

Error Handling using a 'sink'

Error sinks are a way of accumulating errors from API calls related or unrelated into 1 unified error handling pattern. The implementation of a sink requires 2 fundamental design constraints on the APIs supporting this pattern.

  1. Pipelining of errors

    Errors emitted over the course of several API calls are accumulated into a sink which save the error code and message of the first error encountered and can be checked later.

  2. Error proof APIs

    Functions that produce errors must return objects/handles that are marked to trigger no-ops when used in subsequent functions that depend on it.

Consider the following example demonstrating a conventional error handling approach (error values by return/sentinel values) and error handling using error-proof and pipelining.

(A) Conventional error checking patterns using return/sentinel values

OSFile *file = OS_FileOpen("/path/to/file", ...);
if (file) {
    if (!OS_FileWrite(&file, "abc")) {
        // Error handling!
    }
    OS_FileClose(&file);
} else {
    // Error handling!
}

(B) Error handling using pipelining and error proof APIs. APIs that can emit errors take in the error sink as a parameter.

ErrorSink *error = ErrorSink_Begin();
OSFile     file  = OS_FileOpen("/path/to/file", ..., error);
OS_FileWrite(&file, "abc", error);
OS_FileClose(&file);
if (ErrorSink_EndAndLogErrorF(error, "Failed to write to file")) {
    // Do error handling! Assuming the file does not exist, the branch prints
    // the error emitted in OS_FileOpen e.g: 
    // "Failed to write to file. File does not exist/could not be queried for reading '/path/to/file'"
}

Pipelining and error-proof APIs lets you write sequence of instructions and defer error checking until it is convenient or necessary. Functions are guaranteed to return an object that is usable. There are no hidden exceptions to be thrown. Functions may opt to still return error values by way of return values thereby not precluding the ability to check every API call either.

Ultimately, this error handling approach gives more flexibility in how errors are handled with less code by allowing API error checking code to be grouped into logical blocks as seen fit by the programmer.

Error sinks can nest begin and end blocks. Nesting them opens a new scope whereby the current captured error is pushed onto a stack and the sink can be populated again by the first error encountered in that scope.

ErrorSink *error = ErrorSink_Begin();
OSFile     file  = OS_FileOpen("/path/to/file", ..., error);
OS_FileWrite(&file, "abc", error);
OS_FileClose(&file);

{
    // NOTE: My error sinks are thread-local, so the returned 'error' is
    // the same as the 'error' value above because we use per-thread arena
    // allocators to persist error information across calls and threads.
    ErrorSink_Begin();
    OS_WriteAll("/path/to/another/file", "123", error);
    ErrorSink_EndAndLogErrorF(error, "Failed to write to another file");
}

if (ErrorSink_EndAndLogErrorF(error, "Failed to write to file")) {
    // Do error handling!
}

References

This idea was inspired by Mr4th Programming (aka Allen Webster's) error accumulation API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment