Exception Generation is Pure Evil

August 7, 2025

Exception Generation is Pure Evil hero image

Exception generation. You certainly know what this is and have definitely used it, if you’ve programmed even a little.

Here’s news for you: exception generation is very often bad.

In any situation where a try/except (or try/catch) block is used not for “wrapping” side effects like reading files, going to the network, or in an isolated place around a function (for targeted debugging purposes or simply because “there’s just no other way”) — it’s better to get rid of it.

But why? Because such try/except blocks seriously confuse two “processes”:


Think about it yourself — any instruction in a try/except block can throw an exception.

Does it seem like each such block increases the complexity of our program’s execution flow graph exponentially? It doesn’t just seem so.

Modern static analyzers experience… some difficulties with “invisible execution paths”.


Brief historical background.

People started talking about exceptions a very long time ago — in the 40s-50s. The first software implementation appeared in LISP, where there were two functions — ERROR, which was called in case of a program failure, and ERRORSET, which returned, attention — either an error code or NIL.

In other words, this story in LISP is not about the exceptions we have now at all… The concept and implementation evolved, evolved, and developed to the point where instead of error codes we started generating exceptions, namely — creating exception conditions and handling them separately.

The period of mass distribution of exception generation fell on the 80s-90s.

And here’s the result. From a study by Carnegie Mellon from 1999:

Exception failures are estimated to cause two thirds of system crashes and fifty percent of computer system security vulnerabilities. Exception handling is especially important in embedded and real-time computer systems because software in these systems cannot easily be fixed or replaced, and they must deal with the unpredictability of the real world. Robust exception handling in software can improve software fault tolerance and fault avoidance, but no structured techniques exist for implementing dependable exception handling.

I’m writing this article in 2025, and there’s still no structured, reliable, implemented method for exception handling.

Mainstream OOP languages provide the same old try/catch, and frameworks spread several layers of abstractions on top in an even layer, further blurring error boundaries, often providing developers with no ways to handle errors except… wrapping it all in yet another try/catch.

On top of that, exception generation can seriously slow down performance. Here’s fascinating reading from the Java world. Though it’s not limited to just Java — here’s an article about experiments in C++. In Python, especially with 3.11, the situation seems not so dramatic, occurring exceptions slow down code by just 3-5 times.

I’ll just leave this here:

A try/except block is extremely efficient if no exceptions are raised.


They say that once upon a time, long, long ago, the grass was greener and programs were easier to verify. The times before the dominance of exception generation came were the times of Structured Programming — engineers wrote code using conditional operators, loops, and subroutines (functions with one entry and one exit).

Programs flowed, developed naturally “top-down”. Thanks to such determinism, they were easier to read both for people and machines — the first static code analyzer lint was publicly released in 1979. Well, you already know what happened next.

What do we do with all this?

You can act like Google (especially if you write in C++):

We do not use C++ exceptions.

In any other popular object-oriented language you need to… stop using exceptions :)

Or at least don’t bury them deep in code — let the try/catch block wrap a function right where it’s called (outside or inside). Yes, this might add some amount of boilerplate code, but then when exceptions occur, the execution flow will return faster to the deterministically beaten “path”.

Either way, we can find a way to program without using exceptions for flow control.

And you can also remember Golang. No matter how angular and dumbed down it might be, the biggest design decision aimed at strengthening Go code produced by developers is forcing them to handle every error explicitly. Yes, Go has defer/panic/recover, but you’ll have to work hard to twist and mangle the execution flow severely.

And finally — functional programming. Result monad, for example. Curtain. ¯\_(ツ)_/¯

However, I think most developers will find it much easier to simply abandon leaking raise/throw — look at your try/except carefully — maybe you can use if here? Look at raise — how about return?