JavaScript
Article

A Guide to Proper Error Handling in JavaScript

By Camilo Reyes

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!

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 and good practices. We’ll finish by looking at asynchronous code and Ajax.

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 write up will build on concepts explained in the article Exceptional Exception Handling in JavaScript. To paraphrase: “with an exception JavaScript checks for exception handling up the call stack.” I recommend reading up on the basics if you are not familiar. My goal is to explore beyond the bare necessities for handling exceptions. The next time you see a nice try...catch block, it will make you think twice.

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 with unit test.

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

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

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

This unit test is written in Mocha with test assertions in Should.js. Mocha is a test runner while should.js is the assertion library. Feel free to explore these test APIs if you are not already familiar. A test begins with it('description') and ends with a pass / fail in should. The good news is 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.

As shown, error() defines an empty object then 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 can happen to anybody!

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 with unit tests:

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

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

it('returns a value without errors', function() {
    var fn = function() {
        return 1;
    };
    var result = target(fn);
    result.should.equal(1);
});

it('returns a null with errors', function() {
    var fn = function() {
        throw Error('random error');
    };
    var result = target(fn);
    should(result).equal(null);
});

As you can see, this wicked handler returns null if something goes wrong. The callback fn() can point to a legit method or a bomb. The click handler below tells the rest of the story.

(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 just 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 go down well 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. There may be a few cases where doing a silent try-catch is legit. But as far as error handling, this is just bad.

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

The Ugly

Moving on, 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 we just saw. What matters is the way it handles exceptions as shown below with unit test.

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

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

A definite improvement over the bad handler. Here the exception gets bubbled up the call stack. What I like is now errors will unwind the stack which is super helpful in debugging. With an exception, the interpreter will travel 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. But at least I know something went wrong, which is the point of throwing an exception.

The ugly error handler is not as harmful but leads to code smell. Letʼs see if the browser has something up its sleeve to deal with this.

Unwind that Stack

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

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

But, remember I said that the browser is event-driven? Yes, an exception in JavaScript is no more than an event. The interpreter halts execution in the current executing context and unwinds. Turns out, there is an onerror global event handler we can leverage. And it goes something like this:

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. Just 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 that? 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. Granted, the stack property in the error object is not part of the standard yet, but it is consistently available in the latest browsers.

So, one of the cool things we can do with this is log it to the server:

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);
    xhr.send(message);
});

It may not be obvious from the code sample, but this event handler will fire along side the one I just showed above. As mentioned, every handler gets a single purpose which keeps the code DRY. What I like is how these messages get captured on the server.

Here is a screen shot of what this looks like in node:

Ajax log request to node server

This message comes from Firefox Developer Edition 46. With a proper error handler, note that it is crystal clear what the issue is. No need to hide mistakes here! Just by glancing at this, I can see what threw the exception and where. This level of transparency is awesome for debugging front-end code. These messages can get stored in persistent storage for later retrieval, giving further insight on what conditions trigger which errors.

The call stack is super helpful for debugging. Never underestimate the power of the call stack.

Async Handling

Ah, the perils of asynchrony! JavaScript rips asynchronous code out of the current executing context. This means try...catch statements such as the one below have a problem.

function asyncHandler(fn) {
    try {
        setTimeout(function () {
            fn();
        }, 1);
    } catch (e) { }
}

The unit test tells the rest of the story:

it('does not catch exceptions with errors', function () {
    var fn = function () {
        throw new TypeError('type error');
    };
    failedPromise(function() {
        target(fn);
    }).should.be.rejectedWith(TypeError);
});

function failedPromise(fn) {
    return new Promise(function(resolve, reject) {
        reject(fn);
    });
}

I had to wrap the handler around a promise to verify the exception. Note that an unhandled exception occurs, although I have the code block 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, there are two options. 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, the 1970s 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). Their recommendation is to write those blocks at the top of the call stack.

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, you are done! Isn’t it nice that the decision to stay DRY and SOLID is paying off? A global error handler will keep your code nice and clean.

Below is what this exception handler reports on the server. Note that if you’re following along with the demo code, the output you see may be slightly different depending on which browser you’re using.

Async error report on the server

This handler even tells me that the error is coming from asynchronous code. It tells me it is coming from a setTimeout() handler. 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.

  • 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.

  • http://www.difriends.com Ricardo Sánchez

    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

      • http://www.difriends.com Ricardo Sánchez

        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.

  • http://niemyjski.com/ Blake Niemyjski

    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!

  • http://gabinaureche.com/ Gabin Aureche

    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?

      • https://grantwinney.com Grant Winney

        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.

  • 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

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