A couple years ago, I read an excellent article describing the differences, both technical and philosophical, between errors and exceptions in Haskell. It has since stuck with me and guided my development in other languages. The definitions at the beginning were eye-opening:
[W]e use the term exception for expected but irregular situations at runtime and the term error for mistakes in the running program that can be resolved only by fixing the program.
When writing software, I first implement the optimistic code path: the sequence of operations that a well-formed use of the software would produce. This path relies on a sequence of assumptions. After I get the correct uses of my software supported, I start to handle possible violations of those assumptions.
My software thus participates in a contract: if the user provides suitable input, my software will behave as expected. The contract is breached if either the input is malformed or the software has a bug. Each breach of contract is an exception or an error, and the key difference between them is the party responsible. In other words, upon discovering the breach, who can fix it? For exceptions, e.g., a wrong password, it is the user. For errors, e.g., an out-of-bounds array access, it is the programmer.
Since "exception" and "error" have varying meanings in each language community, I will use (hopefully) less ambiguous terms for the rest of this post: surprise and bug, respectively.
Surprises that are not handled gracefully become bugs. It is the responsibility of the programmer to ensure that the program does not crash in response to faulty input.
Bugs that cross an application or library boundary become surprises. The users of a dependency might be programmers, but it is not their responsibility to fix bugs in the dependency. It is, however, their responsibility to deal with the potential for such bugs.
With these definitions, the goal of error-handling is two-fold:
- Eliminate all bugs, fulfilling the program's end of the contract.
- Diagnose all surprises, helping users to fulfill their end.
Bugs must be discovered to be removed. Rather than waiting on unhappy users to come complaining, a good approach to finding bugs is to assert all assumptions in code.
A good assertion will diagnose a violation of an assumption in a way meaningful to the programmer. Since assertions impose an execution penalty but never help users, they should not be present in released code.
I have a background in C++, and the standard
assert() there is quite
powerful. A failed assertion will stop execution of the program and print a
message from the programmer (that should describe the assumption). It often
has compiler support for printing the file name, line number, source code,
and backtrace for the calling expression as well. A special flag instructs
the compiler to disable all assertions.
I prefer to wrap calls to assertions in macros and then use m4 to strip them from release builds.
Surprises are caused by problems in the input or environment. The program may not expect them, but it should be prepared to deal with them. It is obligated to, at the very least, fail gracefully, but good programs will go further and help users to understand and prevent surprises through good diagnostics.
The qualities of a good diagnostic will depend on the application. For Node
applications, it might be a message to
stderr. For browser applications, it
might be an alert or tooltip. Whatever is chosen, the implementation should
be encapsulated within a module so that it can be easily swapped or ported.
I prefer to create a
diag module with a few methods:
Programmers must be careful to identify the party responsible for violations in their software's assumptions. That information determines the appropriate response: a diagnostic for the programmer through an assertion, or a diagnostic for the user through some means deemed appropriate for the application.