Raise On Developer Mistake
To raise, or not to raise – that is the question:
Whether ‘tis nobler in the mind to suffer
The slings and arrows of outrageous return values,
Or to take arms against a sea of non-local control flow– Shakespeare (paraphrased)
When is it appropriate to raise an exception? This is a question that every programmer faces. It’s a complicated topic, with multiple, differing opinions. Here, I want to focus on just one aspect: giving feedback to future developers.
How To Do It
Ask yourself whether the error is caused by a developer mistake/omission. If so, raise an exception.
At a bare minimum, a raised exception indicates that something has gone wrong, which is better than failing silently. A good exception will have an error message that contains exact details about the situation, to aid debugging. It also helps if the message suggests a way to fix the problem.
By raising exceptions, you communicate to future developers when they are not using your code correctly. This is important and useful information!
I know this advice may sound trite. Raise exceptions when something is used wrong – duh! But it actually stems from one of the core trade-offs of dynamic languages: the lack of type checking.
Exceptions Are Feedback In Dynamic Languages
In static, type-checked languages, exceptions play a much lesser role. The compiler catches a lot of mistakes before the code can be run. Developers use the compiler as a feedback mechanism, frequently recompiling their code to check for errors and warnings.
In Ruby, and other dynamic languages, we rely upon tests to uncover errors. One way that tests uncover errors is by causing crashes – unhandled exceptions. Without a compiler, exceptions are the developer feedback mechanism.
Your assertion failures are probably also implemented as exceptions, but here I’m referring to exceptions generated by your own code, as opposed to those generated by your testing framework.
If you’re doing TDD, running tests before the implementation is written, then unhandled exceptions are probably more common than assertion failures in your tests. Exceptions are the feedback in your TDD cycle. The exception’s name and message indicate to you, the developer, what code needs to be written next.
As an example, let’s say you’re implementing something in a Rails app, and you start by writing an integration test. You run the test, and it gives this output:
ActionController::RoutingError: No route matches [GET] "/dog/woof"
This exception tells you that you need to add a route. You add the route, and run the test again, giving you this output:
ActionController::RoutingError: uninitialized constant DogsController
This tells you that you need to create a controller class, and so on.
Example: Missing Routes In Rails
Have another look at how helpful this Rails exception is:
ActionController::RoutingError: No route matches [GET] "/dog/woof"
Is it raised in response to a mistake by the developer? Yes. It only happens in the development environment, where a developer can see it. In production, Rails renders a 404 page instead.
Does the error message guide the developer towards the solution?
Yes.
It makes the next step obvious: go and add get "/dog/woof"
to the routes.
If someone hadn’t specifically implemented this nice exception, one of two things would happen. The testing might pass – a false positive. Alternatively, an exception might still be raised, but it would likely be something ambiguous like:
NoMethodError: undefined method `dispatch' for nil:NilClass
Either way, the actual problem would be unclear. It would force Rails developers to waste time debugging.
Example: Undefined Method
Consider this standard Ruby exception:
NoMethodError: undefined method `lenth' for "abc":String
Did you mean? length
Is it raised in response to a mistake by the developer? Absolutely. This kind of error is never the fault of the user.
Does its error message guide the developer towards the solution? Totally. It:
- Describes the general problem (undefined method)
- Provides detail for debugging (`lenth’ for “abc”:String)
- Suggests a fix (Did you mean? length)
This is one well-designed, developer-friendly exception.
How Often To Raise
It’s not feasible to raise an exception for every possible developer error. If you tried, 99% of your code would be dedicated to raising exceptions.
Focus on the most common and likely error scenarios. Exceptions that will be raised frequently during development have the best return on investment. If an exception will never be raised, then it’s a waste of time to implement it.
Also consider how cryptic the error would be otherwise. Especially vague or complicated errors might warrant a custom exception, to explain the problem clearly.
As a personal example, I was recently implementing something that disallowed cyclic dependencies. Cyclic dependencies would cause infinite recursion, resulting in this error:
SystemStackError: stack level too deep
This message says almost nothing about what the problem actually is.
I expected this situation to crop up occasionally, and it was in a class that was supposed to be generic and reusable, so it was a good candidate for a custom exception. After implementing the exception, the error looks like this:
Container::CyclicDependencies: Cyclic dependencies not allowed
(cycle: "mouse" -> "cheese" -> "milk" -> "pregnant.cat" -> "mouse")
That’s a whole lot better.
Conclusion
If your code encounters a mistake caused by a developer, raise an exception. Exceptions are a developer feedback mechanism in dynamic languages, so raising exceptions is a good way to communicate to developers using your code in the future.
A good error message explains what went wrong, with specific details to aid debugging, and maybe even hints at how to fix it.
You can’t cover every possible type of error situation, so focus on the ones that are most likely to occur during development. You may also want to implement a custom exception for cases that would otherwise have a cryptic, ambiguous error message.
Got questions? Comments? Milk?
Shoot an email to [email protected] or hit me up on Twitter (@tom_dalling).