Error Handling in Node.js
This article was originally published in my blog (affectionately referred to as blargh) on . The original blog no longer exists as I've migrated everything to this wiki.
The original URL of this post was at https://tmont.com/blargh/2014/9/error-handling-in-nodejs. Hopefully that link redirects back to this page.
Error handling in Node is kind of weird. And by "kind of weird", I mean "different than other languages". The semantics of which I shan't get into, but it is my opinion that Node was designed… oddly. At least in terms of error handling. There are several non-obvious ways in which an error can bubble up or otherwise make your program crash, so here's a brief-ish rundown of what to look out for. And how to not do things.
try
, catch
and throw
This is the easy one. Except for the parts where it's not easy. Those are hard.
The normal Java/C♯/C++/etc. way of handling errors is via the familiar
try..catch
construct. It behaves like you would expect:
try {
var json = JSON.parse('nope');
} catch (e) {
console.log(e); //[SyntaxError: Unexpected token o]
}
JSON.parse
is a synchronous function, and therefore it throw
s
errors. That's important to remember. You should never, never, never, ever throw
an exception from asynchronous code. Ever. Srsly.
error
events
error
events are kind of stupid. They are a "special" event within Node, and they're
"special" behavior is that if an error
event is emitted without any
listeners, it crashes the program. Let me repeat that, because repeating things
is something you do when writing to help emphasize something. Or so my high school
English teachers taught me.
error
events without listeners crash programs.
There are no exceptions to this rule, and you can't get around it. Except by listening for
error
event. They are very similar to checked exceptions in Java, so those
of you who sold your soul to the AbstractStrategyFactoryFactory
can rest
easy, as this paradigm should be familiar to you already.
Now, normally, this isn't a huge problem, as it's documented and standardized, so if you care about something emitting an error and want it to not crash your program, you can add a listener and handle it yourself. But it's not so easy.
It's not easy because it's turtles all the way down. Many userland libraries
often do some sort of networking, e.g. connecting to a database. This means
it has to open a socket. Usually that's all abstracted so that the library is
easy to use. Often it's the reason for the library. Abstractions are supposed
to make things easy (and why FactoryFactory
s seem to be so
abundant).
So, now you're connecting to your super awesome NoSQL CouchiakooseandradisDB
database using some random dude's library, which happens to abstract a socket
connection. But is that random dude's library handling the
error
event? If not, you'll need to dig through that dude's probably well-tested
and intelligently-factored code to figure it out. And if it isn't handled,
then you'll have to figure out how to get a hold of the underlying socket
connection and handle the error yourself. Which kind of defeats the purpose
of an abstraction. I assure you this has happened to me before. It's rather
annoying.
The point is, error
events can bite you in the ass, because
they're hard to pinpoint, the stack trace isn't always useful and they
crash your program for seemingly no reason. Be careful and be aware.
Asynchronous errors
Now the real fun. Node is all about those non-blocking system calls. It's
what makes it semi-awesome. Or, at the very least, it's what makes it
different. Depending on how angry you are at random dudes, it might
be the worst thing since the goto
statement.
First, let's distinguish between "asynchronous" and "functions that return stuff via callback". Asynchronous means it's literally not blocking execution. When you call an asynchronous function, it's going off to do something and meanwhile, the rest of your program is executing until that other thing finishes. The act of passing around a callback function does not mean that it is asynchronous. If you can remember this fact, you'll have a leg up on many random dudes. But not in that way, pervert.
The Node convention for passing errors/results in asynchronous functions
is to pass a callback function as the last argument which takes two
arguments: an err
and a result
. The err
is an error, and if it's truthy, that means something broke.
fs.stat('/nonexistent/file', function(err, stat) {
if (err) {
console.error(err);
return;
}
// do something with the stat object
});
And that's pretty much it. Always remember to handle your errors. Oftentimes
the result
will be null
or simply undefined if an error
occurs. Oh yeah. One other thing.
Don't ever mix try..catch
idioms with callback idioms. I'll hate you forever if you do. And it's
bad practice and very confusing, and your stack trace will not be what you think
it should be.
The only time an asynchronous function (or one that returns its result via a callback) should throw an error is if the arguments are bad. Those are programmer errors (e.g. compile-time errors) and should be fixed by a programmer. For example, if a function expects an object but you pass it a string, it's okay if it throws an exception in that case.
function asyncSomething(someObject, callback) {
// okay to throw an exception in this case, although just passing back an error
// in the callback is totally reasonable as well
if (typeof(someObject) !== 'object') {
throw new Error('First argument should be an object');
}
}
A seemingly common error
I've noticed that several well-known and well-used libraries (node-redis and mongoose, specifically) fall into this trap of poor error handling. There are probably others as well, but those are the two where I've noticed this.
Basically, they have a function definition like so:
function doSomething(callback) {
//do something asynchronously, and then...
try {
callback();
} catch (e) {
this.emit('error', e);
}
}
Can you spot the problem?
The problem is that it's attempting to catch
an error
that occurs outside of its scope, and then captures that
error and emits it as if it was an error that occurred inside of
the library. The following code sample should demonstrate why that is
bad:
doSomething(function(err) {
// ReferenceError: foo is not defined, i.e. programmer error
// this should crash the program, as it needs to fixed by a programmer
foo;
});
What happens in this situation? Suppose the library that has the
doSomething
function was not super important to your
application, and you don't really care if it error
'd. So that means
you'll be handling the error
event, logging it, and
ignoring it. What happens when the above code is executed? Will the
program crash as it should?
The answer is a quizzical "No". doSomething
will catch
the undefined error and "re-throw" it by emitting it as an error
event. Since you are listening for those error events and ignoring them,
your program will carry on as if nothing bad happened. But in fact,
something terrible happened. This is an uncaught exception from the
runtime's point of view, and here is what the
Node
docs have to say about those:
An unhandled exception means your application - and by extension node.js itself - is in an undefined state. Blindly resuming means anything could happen.
By catching every random error and emitting it as if it originated from itself,
the doSomething
function is potentially placing Node itself into
an undefined state. This is bad.
You might be tempted to say "But why don't you just add an error event listener and crash the program yourself?" The answer is because that's a super brittle way of handling errors, e.g. doing a regex test on the error message looking for "SyntaxError".
The point is that a library should never, ever capture an error that did not originate
from itself. I actually discovered this by accidentaly spelling a variable wrong
and being extremely confused when the stack trace implied that it originated
from Redis, when in fact it was just me being an idiot. I was even more confused
when my program didn't crash. Redis was not abundantly important to my application,
so if it was unavailable for whatever reason (e.g. connection issues) I didn't really care
and wanted things to keep running. So I had an error
event listener
on Redis which logged the error and let the program keep on running. So imagine my surprise
when a legitimate syntax error didn't crash the program as it should have.
In conclusion, always handle your errors. But don't handle errors that don't belong to you.