We all love when software works perfectly, but things go wrong more often than not. In fact, we use computers largely to help us prevent errors… but only if we build the software with proper error-handling in the first place.
A feature’s “happy path” usually leads to straightforward tests and implementation code - we set up the state of the world, run through a few steps, and check the resulting state. But what about the “not so happy path?”
How should the software behave when the user provides bad input? Or when the software can’t connect to the database? Or when the network goes down?
We want our software to gracefully handle common error conditions, and to maintain system integrity even in the face of catastrophic failure.
On top of all that, we want our code to make sense.
Most programming languages - including Ruby - provide some sort of an exception-handling mechanism for when things go wrong.
But when should you use exceptions, and when should you go with another approach? What other approaches can you use, anyway?
Try this simple rule of thumb: ask yourself, does this error condition arise in an unlikely, or unwanted situation (aka is it “exceptional”)? If so, exceptions will help you out! If not, you might want to represent your failures a different way.
Let’s look at a few examples…
A user provides bad input - is that exceptional? Probably not… users provide bad input all the time, sometimes intentionally! You want to validate user input, but invalid input generally doesn’t warrant using exceptions.
The software can’t connect to the database - is that exceptional? Probably! If the software runs server-side, then the user likely doesn’t know anything about the database. The software abstracts the database from the user… and if the software depends on a database, and it can’t connect to the database, then that represents an exceptional error and you’ll want to use an exception.
Alright, last one: the network goes down. Is that exceptional? Maybe… it happens commonly enough that we can consider it a normal failure condition. However, some software absolutely relies on a network connection to operate. If you can reasonably use a piece of software without a network connection, then a failed network connection probably doesn’t warrant an exception. On the other hand, if the software needs a network connection to perform the simplest task, then a missing network connection probably represents an exceptional condition. And if the network connection dies in the middle of a connected operation… that’s definitely time for an exception!
Choosing whether you want to use exceptions or not will determine how you write your tests. Treating failures as a normal condition can require a bit more code, but usually makes for a more explicit interface. Using exceptions can feel like the right approach to handling errors, but you’ll need to document them well because Ruby has no mechanism for declaring possible exceptions in a chunk of code. If you call some code which raises an exception and you don’t handle it, it can take down your entire program.
We’ll look at failure conditions, exceptions, and their influence on design and tests in future articles. For now, just remember the simple rule of thumb: if a failure condition arises from a normal interaction with your software, then don’t use exceptions. If something goes wrong through no fault of the user, then exceptions can save the day.