Error-handling in JavaScript
Every polished piece of software will have a clear policy, and maybe even a supporting framework, for error-handling. In this post, I want to discuss the primary concerns affecting any error-handling policy and the obstacles posed by JavaScript specifically. I will lay out a clear set of guidelines to follow in my development that I hope others will find useful.
What is an error? What is an exception?
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.
When does one become the other?
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.
What is error-handling?
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.
Eliminating bugs
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.
JavaScript does not have a standard assertion, but most of this can be achieved with a standard exception. Several good libraries exist for writing assertions that throw exceptions, like should.js and expect.js (my favorite).
There are two problems with the library approach. First, it places a dependency on code. (Handling dependencies in JavaScript is a discussion for another post.) The dependency could be removed in deployed code if assertions were stripped (like they should be), but therein lies the second problem: each library implements assertions differently. The Closure Compiler is the only tool I know that will strip assertions, but it cannot be reasonably expected to do so for more than its own assertion library.
I prefer to wrap calls to assertions in macros and then use m4 to strip them from release builds.
Diagnosing surprises
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: error()
, assert()
,
warn()
, and info()
.
Conclusion
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.