JavaScript is swallowing your errors

JavaScript is swallowing your errors
It's late. Do you know where your errors are?

Anticipating software errors and responding appropriately requires considerable thought and effort. How might a program fail? Once it does, is the failure recoverable, and–if so–how?

In the best case, we idenfity and account for the entire range of possible errors preemptively. We map the state to an appropriate error type, provide appropriate feedback to the user, and recover (or terminate) as gracefully as possible.

At the other end of the spectrum are the cases where errors emerge as unhandled exceptions at runtime, or–in the very worst case–pass silently without notice. Modern static analysis can help identify and avert these possibilities, but in a dynamic language like JavaScript such tools aren’t generally available. We’re left to do our best to understand failure, minimize the havoc that failure states can do, and hope it all works out.

To make matters even worse, JavaScript’s particular affinity for evented code tends to breed more pernicious errors than we might find in a single-threaded, synchronous codebase. Overcoming failure is tricky enough in synchronous code; when the failure occurs in an asynchronous future, reasoning through it can get immensely more difficult.

Simply failing

Conside a vanilla callback in node.js.

// expecting `callback(err, ...)`:
function somethingAsync (callback) {
  process.nextTick(function () {
    callback(new Error('Something async failed'));

somethingAsync(function () {
  console.log('Something async finished');

Here we jump off the main call stack, wait a tick, and–if the async function fails, which it will–we return an appropriate error and return. But when we run the code, something surprising happens. It doesn’t break!

Looking closer, we see the problem: we forgot to address the supplied err. Rather than hearing about trouble when somethingAsync didn’t behave as intended, the naïve default gives us silence. No warning. Nothing. Nada. Zilch. Though we know that “something async finished”, we don’t know anything about the state of the system. Did the operation pass, fail, or break early on an exception? It’s left entirely to our imagination.

So who in their right mind would ship a language with error-handling optional? The advantage is that it allows us to capture asynchronous flows in terms of functions–simple, stupid, dead-primitive functions. The problem with using functions in a nearly-typeless language is that it’s up to the developer to decide what to do with each arguments. Since errors are arguments, error-handling falls directly on the mercy of human nature.


Promises take a slightly different approach to the future. When an async outcome is encapsulated in a promise, any failure–including exceptions!–are shepherded down an error-handling path defined by the promise’s .catch implementation.

This means that throwing an Error inside a promise has the same operational effect as rejecting it, requiring even more developer attention to ensure that errors are being addressed appropriately. Replaying the original example, we get some idea of how easy it is for failure to disappear into a promise.

var somethingAsync = new Promise(function (resolve, reject) {
  reject(new Error('Promise failed.'));

somethingAsync.then(function (message) {

Output? Nothing. Where before we at least have to acknowledge the err argument before addressing any additional arguments to a callback, promises will still work if we omit .catch or the rejected side of then.

promise.then(function (message) {
}, function (err) {
  console.error('Failed with', err);

No handler, no error. And since exceptions lead down the same pathway, we’ll miss out on anything thrown as well.

If error handling seemed dangerously optional in callback-passing style, promises go the extra mile. Just like callbacks, promises send errors down a predictable pathway for us to retrieve. Just like callbacks, promises can fail, and our code needs to act accordingly. But unlike callbacks, promises are error-absent by default–and gobble exceptions to boot.

Towards safer code

Like so many of JavaScripts’s rough edges, error handling leaves plenty of rope. Even in synchronous code, the process of anticipating and mitigating failure is entirely up to the developer. But as hard as we try to keep complex logic in the present, any reasonably complex program will run the risk of future, asynchronous failure as well.

The dream is for some kind of enforcement that would ensure that the error branches are covered. Both Chrome and Firefox, for instance, issue console warnings when no handler is available for failed promises. We can devise clever utilities of our own to require that failure, as well. The failOr helper, for instance, ensures that a failure argument is supplied ahead of the successful continuation:

// Force error and success methods for a node callback
function failOr (action, onFail, onPass) {
  if (typeof onFail != 'function' || typeof onPass != 'function')
    throw new Error('Branches not satisfied');

  action(function (err) {
    if (err) onFail.apply(this, arguments);
    else     onPass.apply(this, [], 1));

Used consistently, failOr will require that both error and success branches are present, though it can’t provide any assertions about whether the branches handle their concerns appropriately:

var onError = function (err) {
  console.error('Failed reading file');

var onSuccess = function (data) {
  console.log('File loaded: ', data);

failOr(fs.readFile.bind(fs, './myfile.txt'), onError, onSuccess);

But no matter how clever helpers like this are, we can’t guarantee that the error handling code itself takes the appropriate steps. Failure will slip through. With limited options for static analysis, it’s up to us to diligently cover the error cases, smother them with test cases, and follow up on additional issues as we discover them.

Next up: exception handling