Your system's exceptions are a ticking time bomb
Why eliminating exceptions in favor of functional error handling is a game-changer for your system' stability.
Exceptions have long been the go-to mechanism for signaling and handling unusual circumstances within applications, across many languages.
But let’s set the record straight.
Just because they can be used to announce something went wrong, doesn’t mean they should be used and abused as general purpose for error handling.
Exceptions are, as their name suggests, exceptional. They serve exceptional circumstances — unpredictable events that fall outside the normal operation of a program.
Business rules violations and data validation are expected flows. They’re conditions and scenarios that developers can foresee and thus, should plan for. There isn’t anything “exceptional” to them. They're anticipated pathways that require explicit handling within your application's logic. Using exceptions in such cases fundamentally misunderstands their purpose.
If you’re throwing exceptions expecting its invoker to catch it, then you need to rethink your error handling strategy. Such indiscriminate use of exceptions, especially for routine error handling, obfuscates control flow, degrades performance, and will make your code harder to understand and maintain.
I’m not the only one saying it.
Wikipedia defines exceptions as
anomalous or exceptional conditions requiring special processing – during the execution of a program. In general, an exception breaks the normal flow of execution and executes a pre-registered exception handler;
On Microsoft’s side, the general advice is to thread carefully into exception territory:
The C# language's exception handling features help you deal with any unexpected or exceptional situations that occur when a program is running.
For conditions that are likely to occur but might trigger an exception, consider handling them in a way that will avoid the exception.
Let’s have a closer look and understand the why behind the no-exception reasoning.
Performance costs
It might be really tempting to create exceptions, throw them and think you’ll deal with them later. But throwing exceptions incurs a significant performance cost due to the underlying operations involved - think stack unwinding.
This overhead is particularly problematic in performance-sensitive areas of an application.
Obscured logic and flow interruption
When exceptions are used liberally, the control flow of an application becomes fragmented and less intuitive.
Exceptions are a form of non-local control, acting as glorified go-to calls that interrupt the execution flow. This fragmentation complicates the task of tracing through code when you want to understand how errors are handled, and ultimately leads to inconsistent error management practices.
To put it more bluntly, exceptions cause jumps from one known point in our application to whatever point, in whichever state, often in ways that are hard to reason about. That is if we define that catch point somewhere.
When thinking about the business context of your problem, you should create abstractions that help understanding the domain and flow. Don’t be fooled to think that by crafting MyCustomException : Exception
you’re building that abstraction over your domain. Exceptions act like a uno card that’s gonna pull a reverse on you later on. You think you got rid of the problem, but in fact you just made it more complex.
The False Security of exception handling
Relying on exceptions for error handling gives a false sense of security. The very act of throwing an exception for known error conditions, such as a missing dependency, although necessary in some cases, should be the exception rather than the rule.
By all means, if you really need the application to crash because you cannot continue, do so.
Just keep in mind to ponder really well whether that’s really the case. Don’t make code more complex and harder to maintain just because it’s “easy”.
And no, having a application-wide catch-all is not a great idea either. What are you catching? What are you forecasting to handle and how?
Deceiving nature of methods’ signature
In C# exceptions are hidden and make methods deceiving. When looking at the signature of a method, you’ll be easily tricked into expecting a certain return type. However, that signature is fooling you, by not giving you all the information.
There’s no proper way to know about the possible exceptions it might throw, except from the xml documentation a thoughtful author might’ve created (if any).
Let’s take the FileStream
of Microsoft. We see there’s a bunch of exceptions presented as possible scenarios. But it makes for a horrible developer experience.
You now have to wrap chunks of code in different try statements, interrupting the execution of your function.
Should you have to worry about all these cases as errors?
Couldn’t they have been depicted any other way?
And even if you do your due diligence and cover all cases, what if tomorrow they decide to throw on a different exception?
Here we’re talking Microsoft, which is quite gentle with developers. What if we talk external public packages? What if we talk cross-team packages? What if we talk inner-team?
There’s no proper way for you as a developer to be warned at compile time that you haven’t handled this case. Nothing is forcing you to properly handle the method’s exceptions and what’s worse, unless you dive into each and every method, you’ll find about such cases a tad too late: at runtime.
And don’t get me wrong. This is not a C#-only problem. If you’re tinkering with JavaScript, you know that it too doesn’t have a native way to indicate the throws of a function, once you invoke it.
Promises with no catch will just log silent errors, leading to a terrible user experience. Ever caught yourself annoyed and frantically clicking a button, thinking it did nothing? Of course you did.
***
I think by now it’s became crystal clear why throwing exceptions is so problematic.
So what’s the alternative?
Enter Result Object
In contrast to the traditional exception handling model, adopting result objects for error management offers a more explicit, intuitive, and performant way of handling errors.
A result
object encapsulates the outcome of an operation, clearly distinguishing between success and failure states. This model encourages a more deliberate handling of errors and makes the control flow within applications more transparent.
Enhanced Performance
Without the need to throw exceptions, the performance overhead associated with them is eliminated. Operations can return result objects directly, bypassing the costly process of exception throwing and catching.
Clarity in Control Flow
Result objects make the handling of errors a first-class concern in the application flow. This explicitness enhances readability and maintainability, as success and failure paths are clearly delineated within the code.
Flexibility and Consistency in Error Handling
Using result objects, developers gain flexibility in how and where errors are handled, promoting a consistent approach to error management across the application. This model facilitates easier debugging and testing, as error conditions are explicitly represented in the code.
Honest signatures
Result objects bring errors out of the shadows, depicting clearly the possibility of one to be returned. This explicitness enhances the self-documenting nature of code and aligns with principles of clean code that advocate for readability and maintainability.
Result object example
Incorporating result objects into your applications encourages a design that aligns closely with Domain-Driven Design (DDD) principles. By encapsulating success and failure within a single object, you can create a rich model of your application's domain, making the code more intuitive and aligned with business operations and logic.
Consider a trivial example where a method attempts to validate the properties of a user. Instead of throwing an exception when encountering an error, the method could return a result object indicating the failure and its cause:
This approach contrasts sharply with traditional exception handling, where the method might throw an exception for the same condition, complicating both the method's implementation and its consumption:
By using Result
object, the method's signature truthfully communicates its behavior to callers, including the potential for failure, without relying on side effects like exceptions. This clarity and honesty in method signatures lead to safer, more reliable code.
Conclusion
The conventional wisdom surrounding exception handling is due for a reevaluation.
Exceptions should be reserved for truly exceptional, unpredictable events.
For routine error handling, especially for known error conditions, result objects offer a superior alternative that is explicit, efficient, and conducive to clearer, more maintainable code.
Adopting result objects not only improves the performance and reliability of applications but also enhances the developer experience by making error handling a more integral and transparent part of the programming model.
Pay it forward
Here are this week’s knowledge nuggets that made me pause and think:
Design is an Island - by
Geospatial indexes - by
How LinkedIn Uses Caching to Serve 5M Profile Reads/Sec? by
My learnings from the book "A Philosophy of Software Design" - by
Why should you care about web specifications? - by
If you liked this post, share it with your friends and colleagues.
Thank you for the mention Helen!