handling errors or unexpected situations called exceptions. When we throw an
exception, the normal flow of the program is interrupted, and the control is
passed to a particular block of code called a
catch clause, where we or
whoever uses our code can handle the exception or re-throw it. If there is no
catch clause in the current scope, the exception propagates up to the
following scope until it reaches either a
catch clause or the top global
scope. If an exception reaches the global scope without being caught, it causes
an unhandled exception error and terminates the program.
Exceptions can be thrown by the language itself (for example, when parsing an
invalid JSON string) or by the developer using the
throw statement. The fact
that the developer can terminate the program from pretty much anywhere makes
exceptions extremely problematic. Some of the issues we cause with
- Unpredictability: Any function call can nuke the entire program.
- Inability to recover: Once an exception is thrown and not handled with a
catch, there is no easy way to recover.
- Verbose syntax: Exception handling requires a lot of boilerplate code, which makes it harder to read and understand.
- Impurity: Functions that
throwhave side effects, making them impure and making functional development with them more complex than it needs to be.
- Breaking control flow: Exceptions break the normal control flow of the program, making it harder to reason about.
throw is very common in class-based languages such as Java, so folks
default to exceptions for all kinds of simple errors. On the other side, the
about with a syntax that looks synchronous but also turns
Don’t get me wrong. The problem is not exceptions. The problem is their overuse. Sometimes developers use them for things that are not exceptional, like checking if a property exists or a value is valid. The code turns into a minefield:
“If something doesn’t go as I want, detonate the entire app.”
- Use conditional statements: Instead of throwing an exception when
encountering an invalid input or state, we can use conditional statements,
ternary operators, default values, and
undefinedto check for errors and handle them accordingly. This way, we avoid creating unnecessary objects, keep our control flow explicit and avoid breaking composition with other functions:
- Use optional chaining and nullish coalescing operators: Instead of
throwing an exception when accessing a property or method that may not
exist, we can use optional chaining (
?.) and nullish coalescing (
??) operators to safely access nested properties and provide default values if they are
null. This way, we avoid potential type errors, simplify our syntax and avoid breaking composition.
This article shows why we don’t need
instead. Of course, this does not mean we should never use exceptions. Some
cases still exist where exceptions are appropriate and valuable, such as when
dealing with critical errors that cannot be handled locally or when implementing
custom error types. However, we should be careful and mindful when using
exceptions and avoid abusing them for purposes they are not designed for.
We can write more composable, readable, modular, and robust code by using alternatives such as conditional statements, optional chaining, and nullish coalescing operators. We can also avoid common pitfalls and bad practices leading to bugs or confusion.
So next time you feel tempted to
and ask yourself: Do I want to make the consumer app explode if this fails? I
promise you, almost always, the answer will be no.