JavaScript
Article
By Camilo Reyes

A Guide to Proper Error Handling in JavaScript

By Camilo Reyes

Ah, the perils of error handling in JavaScript. If you believe Murphy’s law, anything that can go wrong, will go wrong. In this article, I would like to explore error handling in JavaScript. I will cover pitfalls, good practices, and finish with asynchronous code and Ajax.

This popular article was updated on 08.06.2017 to address reader feedback. Specifically, file names were added to snippets, unit tests were cleaned up, wrapper pattern was added to uglyHandler, sections on CORS and 3rd party error handlers were added.

I feel JavaScript’s event-driven paradigm adds richness to the language. I like to imagine the browser as this event-driven machine, and errors are no different. When an error occurs, an event gets thrown at some point. In theory, one could argue errors are simple events in JavaScript.

If this sounds foreign to you, buckle up as you are in for quite a ride. For this article, I will focus only on client-side JavaScript.

This topic builds on concepts explained in Exceptional Exception Handling in JavaScript. I recommend reading up on the basics if you are not familiar. This article also assumes an intermediate level of JavaScript knowledge. If you’re looking to level up, why not sign up for SitePoint Premium and watch our course JavaScript: Next Steps. The first lesson is free.

In either case, my goal is to explore beyond the bare necessities for handling exceptions. Reading this article will make you think twice the next time you see a nice try...catch block.

The Demo

The demo we’ll be using for this article is available on GitHub, and presents a page like this:

Error Handling in JavaScript Demo

All buttons detonate a “bomb” when clicked. This bomb simulates an exception that gets thrown as a TypeError. Below is the definition of such a module:

// scripts/error.js

function error() {
  var foo = {};
  return foo.bar();
}

To begin, this function declares an empty object named foo. Note that bar() does not get a definition anywhere. Let’s verify that this will detonate a bomb with a good unit test:

// tests/scripts/errorTest.js

it('throws a TypeError', function () {
  should.throws(error, TypeError);
});

This unit test is in Mocha with test assertions in Should.js. Mocha is a test runner while Should.js is the assertion library. Feel free to explore the testing APIs if you are not already familiar. A test begins with it('description') and ends with a pass / fail in should. The unit tests run on Node and do not need a browser. I recommend paying attention to the tests as they prove out key concepts in plain JavaScript.

Once you have cloned the repo and installed the dependencies, you can run the tests using npm t. Alternatively, you can run this individual test like so: ./node_modules/mocha/bin/mocha tests/scripts/errorTest.js.

As shown, error() defines an empty object then it tries to access a method. Because bar() does not exist within the object, it throws an exception. Believe me, with a dynamic language like JavaScript this happens to everyone!

The Bad

On to some bad error handling. I have abstracted the handler on the button from the implementation. Here is what the handler looks like:

// scripts/badHandler.js

function badHandler(fn) {
  try {
    return fn();
  } catch (e) { }
  return null;
}

This handler receives a fn callback as a parameter. This callback then gets called inside the handler function. The unit tests show how it is useful:

// tests/scripts/badHandlerTest.js

it('returns a value without errors', function() {
  var fn = function() {
    return 1;
  };

  var result = badHandler(fn);

  result.should.equal(1);
});

it('returns a null with errors', function() {
  var fn = function() {
    throw new Error('random error');
  };

  var result = badHandler(fn);

  should(result).equal(null);
});

As you can see, this bad error handler returns null if something goes wrong. The callback fn() can point to a legit method or a bomb.

The click event handler below tells the rest of the story:

// scripts/badHandlerDom.js

(function (handler, bomb) {
  var badButton = document.getElementById('bad');

  if (badButton) {
    badButton.addEventListener('click', function () {
      handler(bomb);
      console.log('Imagine, getting promoted for hiding mistakes');
    });
  }
}(badHandler, error));

What stinks is I only get a null. This leaves me blind when I try to figure out what went wrong. This fail-silent strategy can range from bad UX all the way down to data corruption. What is frustrating with this is I can spend hours debugging the symptom but miss the try-catch block. This wicked handler swallows mistakes in the code and pretends all is well. This may be okay with organizations that don’t sweat code quality. But, hiding mistakes will find you debugging for hours in the future. In a multi-layered solution with deep call stacks, it is impossible to figure out where it went wrong. As far as error handling, this is pretty bad.

A fail-silent strategy will leave you pining for better error handling. JavaScript offers a more elegant way of dealing with exceptions.

The Ugly

Time to investigate an ugly handler. I will skip the part that gets tight-coupled to the DOM. There is no difference here from the bad handler you saw.

// scripts/uglyHandler.js

function uglyHandler(fn) {
  try {
    return fn();
  } catch (e) {
    throw new Error('a new error');
  }
}

What matters is the way it handles exceptions as shown below with this unit test:

// tests/scripts/uglyHandlerTest.js

it('returns a new error with errors', function () {
  var fn = function () {
    throw new TypeError('type error');
  };

  should.throws(function () {
    uglyHandler(fn);
  }, Error);
});

A definite improvement over the bad handler. Here the exception gets bubbled through the call stack. What I like is now errors will unwind the stack which is super helpful in debugging. With an exception, the interpreter travels up the stack looking for another handler. This opens many opportunities to deal with errors at the top of the call stack. Unfortunately, since it is an ugly handler I lose the original error. So I am forced to traverse back down the stack to figure out the original exception. With this at least I know something went wrong, which is why you throw an exception.

As an alternative, is is possible to end the ugly handler with a custom error. When you add more details to an error it is no longer ugly but helpful. The key is to append specific information about the error.

For example:

// scripts/specifiedError.js

// Create a custom error
var SpecifiedError = function SpecifiedError(message) {
  this.name = 'SpecifiedError';
  this.message = message || '';
  this.stack = (new Error()).stack;
};

SpecifiedError.prototype = new Error();
SpecifiedError.prototype.constructor = SpecifiedError;
// scripts/uglyHandlerImproved.js

function uglyHandlerImproved(fn) {
  try {
    return fn();
  } catch (e) {
    throw new SpecifiedError(e.message);
  }
}
// tests/scripts/uglyHandlerImprovedTest.js

it('returns a specified error with errors', function () {
  var fn = function () {
    throw new TypeError('type error');
  };

  should.throws(function () {
    uglyHandlerImproved(fn);
  }, SpecifiedError);
});

The specified error adds more details and keeps the original error message. With this improvement it is no longer an ugly handler but clean and useful.

With these handlers, I still get an unhandled exception. Let’s see if the browser has something up its sleeve to deal with this.

--ADVERTISEMENT--

Unwind that Stack

So, one way to unwind exceptions is to place a try...catch at the top of the call stack.

Say for example:

function main(bomb) {
  try {
    bomb();
  } catch (e) {
    // Handle all the error things
  }
}

But, remember I said the browser is event-driven? Yes, an exception in JavaScript is no more than an event. The interpreter halts execution in the executing context and unwinds. Turns out, there is an onerror global event handler we can use.

And it goes something like this:

// scripts/errorHandlerDom.js

window.addEventListener('error', function (e) {
  var error = e.error;
  console.log(error);
});

This event handler catches errors within any executing context. Error events get fired from various targets for any kind of error. What is so radical is this event handler centralizes error handling in the code. Like with any other event, you can daisy chain handlers to handle specific errors. This allows error handlers to have a single purpose if you follow SOLID principles. These handlers can get registered at any time. The interpreter will cycle through as many handlers as it needs to. The code base gets freed from try...catch blocks that get peppered all over which makes it easy to debug. The key is to treat error handling like event handling in JavaScript.

Now that there is a way to unwind the stack with global handlers, what can we do with this?

After all, may the call stack be with you.

Capture the Stack

The call stack is super helpful in troubleshooting issues. The good news is that the browser provides this information out of the box. The stack property is not part of the standard, but it is consistently available on the latest browsers.

So, for example, you can now log errors on the server:

// scripts/errorAjaxHandlerDom.js

window.addEventListener('error', function (e) {
  var stack = e.error.stack;
  var message = e.error.toString();

  if (stack) {
    message += '\n' + stack;
  }

  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/log', true);
  // Fire an Ajax request with error details
  xhr.send(message);
});

It may not be obvious from this example, but this will fire alongside the previous example. Every error handler can have a single purpose which keeps the code DRY.

In the browser, event handlers get appended to the DOM. This means if you are building a third party library, your events will coexist with client code. The window.addEventListener() takes care of this for you, it does not blot out existing events.

Here is a screenshot of what this log looks like on the server:

Ajax Log Request to Node Server

This log lives inside a command prompt, yes, it is unapologetically running on Windows.

This message comes from Firefox Developer Edition 54. With a proper error handler, note that it is crystal clear what the issue is. No need to hide mistakes, by glancing at this, I can see what threw the exception and where. This level of transparency is good for debugging front-end code. You can analyze logs, giving insight on what conditions trigger which errors.

The call stack is helpful for debugging, never underestimate the power of the call stack.

One gotcha is if you have a script from a different domain and enable CORS you won’t see any of the error details. This occurs when you put scripts on a CDN, for example, to exploit the limitation of six requests per domain. The e.message will only say “Script error” which is bad. In JavaScript, error information is only available for a single domain.

One solution is to re-throw errors while keeping the error message:

try {
  return fn();
} catch (e) {
  throw new Error(e.message);
}

Once you rethrow the error back up, your global error handlers will do the rest of the work. Only make sure your error handlers are on the same domain. You can even wrap it around a custom error with specific error information. This keeps the original message, stack, and custom error object.

Async Handling

Ah, the perils of asynchrony. JavaScript rips asynchronous code out of the executing context. This means exception handlers such as the one below have a problem:

// scripts/asyncHandler.js

function asyncHandler(fn) {
  try {
    // This rips the potential bomb from the current context
    setTimeout(function () {
      fn();
    }, 1);
  } catch (e) { }
}

The unit test tells the rest of the story:

// tests/scripts/asyncHandlerTest.js

it('does not catch exceptions with errors', function () {
  // The bomb
  var fn = function () {
    throw new TypeError('type error');
  };

  // Check that the exception is not caught
  should.doesNotThrow(function () {
    asyncHandler(fn);
  });
});

The exception does not get caught and I can verify this with this unit test. Note that an unhandled exception occurs, although I have the code wrapped around a nice try...catch. Yes, try...catch statements only work within a single executing context. By the time an exception gets thrown, the interpreter has moved away from the try...catch. This same behavior occurs with Ajax calls too.

So, one alternative is to catch exceptions inside the asynchronous callback:

setTimeout(function () {
  try {
    fn();
  } catch (e) {
    // Handle this async error
  }
}, 1);

This approach will work, but it leaves much room for improvement. First of all, try...catch blocks get tangled up all over the place. In fact, 1970s bad programming called and they want their code back. Plus, the V8 engine discourages the use of try…catch blocks inside functions. V8 is the JavaScript engine used in the Chrome browser and Node. One idea is to move blocks to the top of the call stack but this does not work for asynchronous code.

So, where does this lead us? There is a reason I said global error handlers operate within any executing context. If you add an error handler to the window object, that’s it, done! It is nice that the decision to stay DRY and SOLID is paying off. A global error handler will keep your async code nice and clean.

Below is what this exception handler reports on the server. Note that if you’re following along, the output you see will be different depending on which browser you use.

Async Error Report on the Server

This handler even tells me that the error is coming from asynchronous code. It says it is coming from a setTimeout() function. Too cool!

Conclusion

In the world of error handling, there are at least two approaches. One is the fail-silent approach where you ignore errors in the code. The other is the fail-fast and unwind approach where errors stop the world and rewind. I think it is clear which of the two I am in favor of and why. My take: don’t hide problems. No one will shame you for accidents that may occur in the program. It is acceptable to stop, rewind and give users another try.

In a world that is far from perfect, it is important to allow for a second chance. Errors are inevitable, it’s what you do about them that counts.

This article was peer reviewed by Tim Severien and Moritz Kröger. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

  • Jeremy Nagel

    @camilo thanks for the article. What advice would you give for error handling on a third party JS API that should not touch window.onerror? (Clients inject our code onto their page and don’t like it when we override their functionality)

    Only way I can see to do it is to have a Try/Catch at the top of the execution context. Any other thoughts?

    • Benjamin Gruenbaum

      Propagate your errors to the users of the library and document how they are propagated. If it’s something your library can handle – handle it, if it’s not – just throw the error.

    • Camilo Reyes

      You aren’t overriding window.onerror when you window.addEventListener(). The cool thing is these listeners get _appended_, so it shouldn’t matter what the client code does.

  • Benjamin Gruenbaum

    Hey, you can collect the errors in your promises similarly to how you do window.onerror, by doing a `window.addEventListener(“unhandledrejection”`

    • Camilo Reyes

      Good point, you probably could. The only sucky thing is you must Promise all the thingz.

      • Benjamin Gruenbaum

        “”Promising all the things”” is a single command with a library like bluebird. This is not required for the aforementioned functionality though.

      • Guilherme

        Couldn’t you use both onerror and unhandledrejection?

    • Scato

      I think promises are very useful in async error handling. Global error handlers are very useful for logging, but promises give you a way to recover from errors. It’s the same as catch blocks: you can just log the exception, or you can recover from an error with a retry, default value, etc.

  • why “proper”? it would be easier with monads instead of long jumps to random points in the stack, don’t you think so?

    • Camilo Reyes

      Not sure I follow, monads are function containers right? So an error event would unwind that stack too. Also, it doesn’t solve the problem with asynchrony

      • There are several types of monads, read about the Maybe and Either, that I think are the most useful in JavaScript. The idea is error handling without enter in panic mode.
        They are functors (with map function), not functions but they can contain a function as well.

  • Fish Taco

    Nice article, could use some tidying up. There are spelling & grammar errors, and it would be easier to follow if you say which snippets correspond to which parts of the git repo.

    • Camilo Reyes

      Awesome feedback, will remember for next time. As for the errors, this is about error handling, correct?

  • Qodesmith

    Thanks so much for this! I was completely unaware of the onerror event in JavaScript. This will make life much easier for me.

    • Camilo Reyes

      Nice, you just made my day

  • Yoni

    Important thing to note is that you can only get the error information if the script is loaded from the same domain, or by allowing CORS. otherwise you would just get “Script error.” as a message, without stack trace.

  • Qodesmith

    I tried implementing a small example, but the ‘error’ event either doesn’t fire or something else is off. Here’s the simple code:

    // First, the error event listener:
    window.addEventListener(‘error’, function (e) {
    var error = e.error;
    console.log(error);
    });

    // Second, the function that will throw the error:
    function test(fxn) {
    return fxn();
    }

    // Third: make the error happen:
    test(‘hi’);

    Running the above code only shows the typical Uncaught TypeError in the console. The event listener never gets triggered. Running on Chrome. Any thoughts?

    • Camilo Reyes

      Hmm… At a glance, could be a race condition. Are you sure the event gets registered BEFORE you throw the exception?

      • Qodesmith

        Yes. I’ve even tried it right in the console. Copy/paste the first two portions, then type the 3rd portion manually. You will see that the event never gets triggered.

  • I’d highly recommend checking out our OSS project exceptionless: https://github.com/exceptionless/Exceptionless.JavaScript We have a javascript client that uses TraceKit and captures and reports all of your nasty exceptions and much more to to a centralized service. Feel free to take a look at our code to learn more and send us your feedback!

  • Error logging helps a lot to troubleshoot bugs but also to be more confident. I’d recommend to have a look at Bugsnag, it’s a fantastic service to track down errors in production: https://bugsnag.com

  • articicejuice

    Sorry, but when I see a Windows Command prompt screenshot, I stop reading.

    • Nilson Jacques

      According to Stack Overflow’s 2016 developer survey, some 52% of developers use Windows. Is there any reasoning behind disregarding what someone has to say because of their choice of desktop OS?

      • His loss. Great article. Thanks for sharing @camilo_reyes:disqus. I had started cluttering my code with try/catch blocks when I thought, there’s gotta be a better way. Then I came across this…

        • Camilo Reyes

          Awesome! So glad you found it useful.

  • ezekiel

    I’d like to add that the uglyHandler isn’t bad at all, I read about wrapping third-party APIs is a best practice to minimize dependencies. (Clean Code p.109 chapter 7)

    • Camilo Reyes

      Ah, good point, still on my bucket list to read Clean Code. I hear it’s an awesome book!

      The point I make is avoid reducing clarity when errors occur. Note this ugly handler grabs the original error. Then, throws a generic error with no information.

      If your wrapper is there to add details to an error, I totally agree. There are effective wrapper patterns that add more clarity to what is going on.

      • Te ENe Te

        Clan Code Brook is a geme!

  • Mohamed Hussain

    Thanks Camilo for the article….What about the If code has to be resumed from the next line where error occurred, by catching the error globally we might come out of the execution path and not executing the remaining code?

    • Camilo Reyes

      Not sure why you’d want to do that. An error in your program is an exception. An exception occurs outside the expected flow. It is not coupled to programming logic, at all.

      That said, in JavaScript, you can encapsulate cross-cutting concerns through an event. Depending on what you need to do.

      • Mohamed Hussain

        for ex :

        function calc(argument) {
        var add = addition();
        var sub = subtraction();

        return {
        add: add,
        sub: sub
        }

        }
        function addition(operands) {
        // throws error
        }
        function subtraction(operands) {
        // no error
        }
        calc();

        as in above code , error is in addition, I would like to continue my flow to next line subtraction even though error is found in addition, i do not want the flow to stop since error is in one part of my code, want graceful degradation

  • Zorgatone

    I don’t really get where this ‘target’ variable is defined, everytime

  • Ravikumar S S

    I’am using window.onerror event but this event not catch the try method error.How to catch the try method error?Forgive me if the english is wrong.

    Example:
    window.onerror = function(msg, url, lineNum) {console.log(msg)}
    function referenceErrorCatch(num) {
    try{
    console.log(number)
    }
    catch(e){}
    }
    referenceErrorCatch(04)

    • Camilo Reyes

      Get rid of the try / catch and the onerror will catch it instead :)

Recommended
Sponsors
Get the latest in JavaScript, once a week, for free.