IfFailGo and the language feature / pattern principle

Exceptions are “just” an undeclared return value, which cascade by default.

COM HRESULT

I worked on a C/C++ project that used COM’s HRESULT extensively - just about every function returned an HRESULT. HRESULT is a 32-bit integer where negative values indicate some kind of failure (out of memory, invalid argument, etc.) and non-negative values indicate some kind of success.

Even though HRESULT can return billions of distinct values, in practice we mostly just cared about success or failure, and on failure we’d just propagate the value up to the caller without further inspection. Like this:

    HRESULT hr = some_function(...);
    if (hr < 0) {
        return hr;
    }

This causes every 1-line function call to take up 4 lines (5 if you add whitespace).

It also gets complicated if you need to release allocated resources:

    x = malloc(...);
    HRESULT hr = some_function(...);
    if (hr < 0) {
        free(x);  // remember this!
        return hr;
    }

    y = malloc(...);
    HRESULT hr = some_function_2(...);
    if (hr < 0) {
        free(x);   // and this!
        free(y);   // also this!
        return hr;
    }

The project adopted a macro to help:

#define IfFailGo(x) do { \
        hr = (x); \
        if (FAILED(hr)) goto Error; \
    } while(0)

It can be used like this:

{
    HRESULT hr = S_OK;

    x = malloc(...)
    IfFailGo( some_function(...) );
    y = malloc(...)
    IfFailGo( some_function_2(...) );

Error:
    // clean up resources as needed
    return hr;
}

This is a pattern for doing what exceptions do. In that time and place, exceptions were a new feature in C++ that were viewed with skepticism, so the developers ended up basically implementing exceptions by hand.

I don’t think IfFailGo() was ever officially published outside of Microsoft but I can see it leaked in a couple places, like here and here.

Exceptions are returns

Above I said “exceptions are “just” an undeclared return value, which cascade by default.” Here are some C# nearly-equivlanet code snippets that illustrate this principle:

Option 1

    F();

    try {
        F();
    } catch (MyException e) {
        Console.WriteLine(e.value);
    }

int F() {
    ...
    throw MyException(42);
    ...
}

Option 2

record PossibleResult<T>(bool failed, T? value);

    result = F();
    if (result.failed) return result;

    result = F();
    if (result.failed) {
        Console.WriteLine(result!.value);
    }

PossibleResult<int> F() {
    ...
    return PossibleResult(true, 42);
    ...
}

(Note the similarity to JavaScript’s Promise.)

Remember

Every software design pattern is a potential programming language feature. Every programming language feature is an encoding of a design pattern. When a language does this, it makes the design pattern conveniently available to everyone and lets us share our understanding of the pattern, but it also locks us in to just one way to do that pattern.

If you’re designing a programming language, think hard about which design patterns you want to lock down and make universally available. If you’re writing code, you can sometimes replace use of a language feature with a design pattern, and this can give you more flexibility that might be useful, for a price.

Caveat

(All code typed from memory, probably has mistakes.)

Written on December 12, 2024