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 throws 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 FactoryFactorys 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.