Exceptions are just fancy gotos

Author: Chloé Lourseyre

About goto

Back to basics: why is goto evil?

I could just throw the link to the article E. W. Dijkstra wrote (Go To Statement Considered Harmful (arizona.edu)), which explains pretty well why goto is evil, but I’d like to go a little further than that.

I will provide here a list of reasons to not use goto, summarizing the article of Dijkstra and adapting its reasoning to modern programming:

  1. The goto control flow is unstructured, unlike the other control flows in C++. if and loop statements are geographically linked to the code they control. It is either linked to a block (with its own lifecycle) or a single instruction. while reading it, any developer can see and understand what code is nested within the if or the loop and where it ends. Functions, another control flow in C++, are access points. Their signature is a contract that is secured by the compiler. You give input, you get a return value. gotos are unstructured as they are a one-way trip across code, with no geographical attachment like ifs and loops and no entry-exit guarantee like functions. This is very prone to unmaintainable, spaghetti code. Long story short: it breaks the program continuity.
  2. Regarding what it can do, goto is probably the most unsafe keyword in the language. I won’t give lengthy examples here, but you can try it out yourself: the number of stupid things you are allowed to do with goto is surprisingly high. And if you do stupid things, you may have crashes, sometimes random, memory corruption, undefined behavior…
  3. Here and today, in the era of modern C++, nobody uses goto anymore. That means most of us are unfamiliar with it. Most developers know they shouldn’t use it, but a whole lot of them don’t know why. Thus, if they ever encounter a goto in their codebase, they are most likely to either refactor the code (and possibly cause a regression, since they are unfamiliar with it) or leave it that way without trying to understand what it does. Since it’s complicated to use, the fact that nobody uses it regularly is yet another argument against it.

There are specific contexts and language where goto can be considered a good practice, but in modern C++, goto is a no-go.

About control flow and spaghetti code

The first point of the previous section (which is the main argument against goto) is talking about control flow and spaghetti code. But why is this important?

You can see an execution of your program as a rope that is unwound alongside your code as it is executed. As long as there are only simple instructions, the rope is unwound normally. When there is control flow, the rope will behave differently.

When the execution encounters a for or while, the rope will trace loops around the attached block, one rope loop for each execution loop that is performed. When the execution encounters an if (and possibly an else), the rope may or may not jump above the block of statements attached to it, depending on the condition, then continuing its course.

When the execution encounters a function, a strand of the rope will do a curl where the function is, then come back at the rope when it’s over, making the rope whole again.

However, when a goto statement is encountered, sometimes you will have no choice but to stretch the rope across your entire code to reach the target label. If there are multiple gotos at multiple locations, then the rope will cross itself over and over again.

If you have good control flow, then you will be able to follow the rope from the beginning to the end. But if your control flow is messy, then the rope will overlap itself all over and you will have trouble understanding any of it. This is what we call spaghetti code, a control flow that looks like a plate full of spaghetti.

Since gotos make the rope cross over the program, it is very prone to spaghetti code.

Is goto really evil?

But, despite all that, can’t we imagine a simple, safe way to use goto? After all, goto is only evil if it’s used to make spaghetti code, but if we write a code that only uses goto locally, in a closed and controlled space, then the code would not be very spaghetti-ish, would it?

There are designs where the use of goto makes the code clearer than with the use of other control flows.

The classic example is with nested loops:

//...

bool should_break = false;
for (int i = 0 ; i < size_i ; ++i)
{
    for (int j = 0 ; j < size_j ; ++j)
    {
        if (condition_of_exit(i,j))
        {
            should_break = true;
            break;
        }
    }
    if (should_break)
        break;
}

//...

If we write it using goto, it will be shorter and a bit clearer:

// ...

for (int i = 0 ; i < size_i ; ++i)
{
    for (int j = 0 ; j < size_j ; ++j)
    {
        if (condition_of_exit(i,j))
            goto end_of_nested_loop;
    }
}
end_of_nested_loop:

// ...

See? goto is not so useless after all!

So should we use this keyword in those specific cases where it makes the code clearer? I think not.

It’s hard to find a variety of examples where goto is better than any other control flow, and nested loops like this one are very scarce. And even if the code is shorter, I don’t find it clearer. There are 2 block levels of difference between the goto and its label, making the jump from the former to the latter counter-intuitive. The human factor remains a huge problem.

So is goto really evil? No, but it’s still an absolute bad practice.

About exceptions

Exceptions: the modern way to break control flow

Exceptions are a way to manage errors in your code. You can also use it as a standard control flow, since you can customize your own exceptions, throw them and catch them however you want.

I like to see exceptions as “dangling ifs”: if your code performs adequately, everything will be good, but if something is off, you just throw everything upwards, hoping that something in the higher-level program will catch it.

Exceptions do break the standard control flow. To take the image of the rope again, when you call a function that can throw an exception, then a strand of the rope will go into the function (just like before), but you’ll have no guarantee that the strand will be returned to the rope and the place where you called the function. It can be re-attached anywhere above in the call stack. The only way to prevent that is to try-catch all exceptions when you call the function, but this is only possible if you know what to do in every case of error.

Moreover, when you write a function in which you may throw an exception, you have no way to know if and where it will be caught.

Though this is not as bad as a goto, because it is more controlled, you can still easily write spaghetti code. With this hindsight, we can consider that exceptions are a fancy, “modern” equivalent of goto.

We can even write the nested-loops-escape with exceptions:

//...

try 
{
    for (int i = 0 ; i < size_i ; ++i)
    {
        for (int j = 0 ; j < size_j ; ++j)
        {
            if (condition_of_exit(i,j))
                throw;
        }
    }
}
catch (const std::exception& e)
{ /* nothing */ }

//...

I wouldn’t recommend it though. Exceptions are evil.

Are exceptions really evil?

…are they? Not really.

In the previous section, I stated that goto isn’t inherently evil, but is bad practice because it is really error-prone for what there is to gain from it. The same goes for exception: it’s not pure evilness, it’s a just feature.

In my opinion, unlike goto, there are ways to use exceptions safely.

One major difference between exceptions and gotos

To understand how to correctly use exceptions, we must understand the differences between them and gotos.

Exceptions do not send the execution somewhere undefined, it sends the execution upward in the stack. It can be a few blocks upward (like in the nested-loops example) or several function calls upward.

When to use exceptions?

Sending the program execution upward is still pretty undefined, and in most cases, we don’t want to break the program continuity.

In most cases.

There is one specific situation when we want the execution to stop abruptly: when someone misuses a feature in a way that could cause an unwanted behavior (or worse).

When you write a feature, you want to be able to stop any degraded state from happening, and instead send the execution upward and say “something unexpected happened, I can’t proceed”. Not only this will prevent any unwanted behavior, but it will also warn the user that they misused your feature, forcing them to either correct their use of it or handle the error they caused.

When you write a feature, there is a virtual wall between you and the user, with a tiny hole that is represented by the feature’s interface. It’s up to each of you to handle how the fictional rope behaves on each side of the wall.

A feature can be a whole library or a single class, but as long as it’s encapsulated, it’s okay to use exception as part of their interface.

A good example of that is the at() method of std::vector<>. The method’s goal is to return the nth element of the vector, but there is a chance that the user ask for an out-of-bound element. In that case, throwing an exception is a good way to stop the vector from causing an undefined behavior. If the user catches the exception, they can write a code to execute in case of OOB index. If not, then the program stops and indicates that an out-of-bound has been raised, forcing the user to secure their code or handle the degraded state.

In any case, you must document every exception your code can throw.

Wrapping up

I feel like many of the articles I write could just be concluded by “just don’t be stupid”, but I always try to give a good summary anyway, because not being stupid is actually harder than it looks (I am myself sometimes quite stupid, and it happens more times than I would admit).

It is especially hard to not be stupid with exceptions, considering how easy it is to blow everything up. I will try to summarize the good practice to apply regarding exception in three points:

  • Do not use exceptions for flow control.
  • You can use exceptions as part of your feature interface.
  • Document every exception your feature can throw.

Thanks for reading and see you next week!

Author: Chloé Lourseyre

3 thoughts on “Exceptions are just fancy gotos”

Leave a Reply