Why Return Codes Make Me Mad


In which I go on and on complaining about return codes, letting off some steam.

So, return codes. They make me mad. When given the choice, I always prefer exceptions.

Let’s say you have a function getValue(), which returns, well, the value of something. However, if something went wrong during setup, the value might never have been set, and we need to detect that. What we would like to write is

type value = getValue(); //For some type bool / int / WhateverClass

We do however need to detect error conditions. Either we use exceptions, or we use return codes. We can do either

try {
    type value = getValue();
} catch (NotInitializedException&) {
    //handle
}

or

type value;
bool ok = getValue(value)
if (!ok) {
    //handle
}

Either way you need to look out for errors, and maybe one isn’t so much worse than the other. What return codes do to the flow of you code however, is much worse. Here are some of the things that annoy me the most:

The size of the code explodes
An innocent, readable line like

if (getValue() == whatever)

turns into this mess:

type value;
getValue(value)
if(value == whatever)

Which one is easiest to read?

And of course, you can’t do this anymore:

finalResult = doStuffWith(getValue());

Variables have intermediate, invalid values
In the previous example, value needs to be constructed before getValue() is called. There’s a whole list of problems with this:

  • If it is a fundamental type (int, float, bool etc.) you have to manually initialize it, or you will have a dangerous uninitialized variable.
  • Some types don’t even have a default constructor
  • Some types are expensive to construct
  • Some constructors have side effects
  • value will have a meaningless value for a (short) part of the life of the program

You cannot use const variables.
I always make variables const whenever I can. This helps with program correctness, because I will never accidentally modify them, or pass them to a function that modifies them. But now you can’t do

const type value = getValue();

you have to do

type value; //Can't use const
getValue(value) //because it's modified here

Error checking code will infect your code and make it unreadable

Instead of handling errors in one place like this:

try {
    const type value = getValue()
    const othertype othervalue = getRelatedValue(value);
    doStuff(othervalue);
} catch (NotInitializedException&) {
    //handle;
}

You have to check for errors all the time:

type value;
bool ok = getValue(value);
if (ok) {
    othertype othervalue;
    bool otherok = getRelatedValue(othervalue, value);
    if (otherok) {
        doStuff(othervalue);
    } else {
        //handle
    }
} else {
    //handle
}

Look at this mess! Notice how the error handling code gets intermixed with the happy code. In the version of the code using exceptions, this is not necessary, as the rest of the try block is automatically skipped when an exception is thrown.

Constructors can’t even return a return code
The return value of a constructor always is the constructed object, so here you don’t even have a choice. If you don’t want to use exceptions, you need to remember that the object is now in an invalid state. So either you hope your users remember to do

SillyType s;
if (!s.gotProperlyConstructed() {
    //handle
}

or you need to make the newly constructed object remember that it is invalid, and return that as an error code indicating that every time someone calls a method on it from now on.

The code does not say what it really means
An argument to a function is an argument to a function. The return value of a function is the return value of a function. When reading a piece of code, this is the natural way to reason about it. When arguments are suddenly used for return values, the natural flow of the code is broken.

That’s not all
In addition, there’s a host of other problems like:

  • It’s easy to forget to check a return code, leaving your program silently in an invalid state. However, if you forget to catch an exception, you program blows up and at least you know something is wrong.
  • Operators can’t use return codes.
  • Exceptions tend to have useful information attached to them, describing the error. Return codes are usually enums, integers or booleans. (This can be fixed though, by returning an error object instead of an error code.)

Readable code
In all of the examples above, return codes lead to code that is less readable. And for me, readable code is extremely important. When your code is unreadable:

  • It is easier for bugs to hide
  • It is harder to make changes
  • It takes longer to get to know the code for new programmers (which might be yourself, half a year later)
  • It is usually harder to write tests
  • It is scary to refactor because you don’t understand the code fully, so you end up patching on another few lines and another few lines instead

In short, unreadable code leads to more bugs, longer development time for new features, more time spent in the debugger, and more time spent on maintenance. This is why I hate unreadable code. This is why return codes make me mad.

What can you do?
For new projects, go with exceptions. For legacy code you usually need to stay with the existing style. If all the code around you uses return codes, you probably should too. I would however argue that there still can be a place for exceptions: If you are extending the code base with an isolated module, or doing a major refactoring of one, and the interface to the rest of the code is small, use exceptions internally. Then catch the exceptions on the boundary and translate to return codes. After a while, you will gradually move towards exceptions and more readable code for the entire code base.

If you enjoyed this post, you can subscribe to my blog, or follow me on Twitter.

25 thoughts on “Why Return Codes Make Me Mad

  1. Great article! Btw, Common Lisp also takes exceptions to the next level, allowing restarts. Imagine writing code like this:

    if (something_is_wrong) {
    sometype restart_value = throw some_exception(some, args);
    do_something_sensible_instead();
    }

    and in the other end:

    try {
    call_function_with_restart();
    }
    catch (std::restartable_exception& e) {
    if (e.something()) {
    e.restart(some_code);
    }

    }

    Here, e.restart() will never return, and jump back to the point of the throw, returning the argument
    passed from throw.

    I also found an error:
    SillyType s();
    Creates an object or declares a function?

      1. You are correct to not expect to see this in the next standard, but I don ‘t know how unfortunate that it.

        In “The Design and Evolution of C++,” Bjarne Stroustrup’s book on how C++ got to be C++, he discusses how exceptions were defined for C++. There was indeed a debate on “Resumption vs. Termination.” (See Section 16.6 of the book.)

        After careful consideration and discussion of experiences with other languages, resumption was rejected in favor the form we have in C++ today. I’ll let you read the book for the technical details, but I very much doubt that it will be considered again in future standards.

        1. I didn’t know there had been a debate about it, I mentioned it more tongue-in-cheek. It certainly feels more like a Lisp-y feature than a C++-y one. Thanks for reminding me of that book by the way, I still haven’t gotten around to reading it.

  2. Andrei Alexandrescu presented an idea at C++ and Beyond 2012 called Expected. The idea is that a function returns an Expected, which behaves like a std::future. When you call Expected::get(), it returns the value, or throws the exception that would have been thrown – the exception that prevented the value’s creation.

    You would use it this way:
    int Foo();

    int result = Foo();
    if (result == 10) { … }

    Becomes to

    Expected Foo();

    Expected result = Foo();
    if (result.get() == 10) { … }

    It provides a couple of things that you can’t do with normal exceptions, notably you can have several exceptions “active”, ie, you can do the following

    Expected resultA = Foo(); // potentially an exception
    Expected resultB = Foo(); // potentially an exception
    Expected resultC = Foo(); // potentially an exception

    1. That’s a very interesting use of stored exceptions! It does make a lot of sense to do this for futures of course, but I am not sure I would use them in this way.

      It will be interesting to see if this idea catches on, and to see some scenarios in which this will be useful. Generally I want my exceptions thrown as soon as possible.

    1. Joe,

      I’m convinced.

      Anders’ insightful, well-reasoned and thoughtful presentation had me believing that exceptions were superior to return codes for all the good software engineering reason he presents here.

      But you’ve totally turned me around. I didn’t realize that Anders’ (who regularly posts insightful thoughts on improving the state of the art of software engineering) had created a post that was “lame.” Or that, notwithstanding the score of well-reasoned and presented problems presented here, return codes are “fine.”

      Your well-researched and articulately presented, point-by-point response was powerful and convincing, not to mention witty and concise.

      Thanks for contributing to the discourse. Your quest for knowledge, your ambition to better your professional practice, your zest for constant improvement, your uncompromising opposition to mediocrity in code, and your generous nature in sharing the wisdom of your experience is a both a inspiration and a gift to the large software engineering community.

      Jon

        1. Joe’s comment was unproductive, and didn’t contribute anything to the discussion. Maybe the post was indeed lame, maybe return codes are just fine, but a comment like that with 0 arguments has exactly 0 value. I think Jon’s satirical response was appropriate.

  3. We use return codes over Exceptions currently, and for a reason that this article does not cover:
    Exceptions are many times slower than return codes.

    For most programmers they wont even notice the difference, but we deal with realtime processes (~.01ms-10ms response times) in which functions are iterated hundreds, thousands or even millions of times. The overhead with using exceptions has proven to be unacceptably slow for our use.

    1. The model with exceptions is that if an exception is thrown, then you are bailing out and will not complete the operation (you don’t have a loop of thrown exceptions). So the concern is correctness, not performance. This is the correct model for most users. But there are circumstances, and it sounds like your’s is among them, Jonathan, where even in the failure case, the response window is very tight. (In other words, it is okay to fail, but it is not okay to be slow returning.)

      In such circumstances (which I think are rarer than most people believe), exceptions may not be the optimal error handling approach.

      1. If you use exceptions only in truly exceptional cases then the performance argument works the other way around. In the non-throwing case exceptions are usually less overhead than return codes (because there is not checking code). It does depend on the implementation, but most modern, mainstream, compilers use the table based approach.
        For non-exceptional cases I’d tend to prefer an ADT-based approach (such as optional/ expected – as Simon well covers in his response), although if performance is really tight that may be too much overhead, too – but I’d only give them up after profiling.

  4. I agree with the majority of your points here, but I think most of them are addressed to some extent by something like std::expected with a monadic interface. Point by point:

    1. The size of the code explodes
    Example given:
    type value;
    getValue(value)
    if(value == whatever)
    With std::expected: if (auto val = getValue(); val && *val == whatever)

    2. Variables have intermediate, invalid values
    With std::expected: no out parameters, so not a problem

    3. You cannot use const variables
    With std::expected: no out parameters, so not a problem

    4. Error checking code will infect your code and make it unreadable
    Example given:
    type value;
    bool ok = getValue(value);
    if (ok) {
    othertype othervalue;
    bool otherok = getRelatedValue(othervalue, value);
    if (otherok) {
    doStuff(othervalue);
    } else {
    //handle
    }
    } else {
    //handle
    }
    With std::expected + monadic interface:
    getValue().map([](auto value) {
    getRelatedValue(value).map([](auto othervalue) {
    doStuff(othervalue);
    });
    });

    5. Constructors can’t even return a return code
    With std::expected: Make constructors simple and private, create objects using factory functions returning std::expected

    6. The code does not say what it really means
    With std::expected: Return values are return values again

    Of course, there are many places where exceptions are simpler and cleaner, and even more efficient (e.g. when you never actually throw), but there are situations where you either can’t or shouldn’t use them (e.g. where “failure” is expected, in some embedded environments).

    Regardless, good article!

      1. I wholeheartedly agree! I wasn’t aware of the std::expected proposal until now, thanks for bringing it to my attention! Do you know the status of it?
        I’m a big fan of maybe types such as Rust’s Option and Result, which seem to correspond to C++17’s std::optional and the proposed std::expected, respectively. If I were to write the article again today (it’s from 2012) I’d include recommendations to use those.

Leave a comment