Error handling in JavaScript is a risky business. If you believe in
Murphy's law , then you know perfectly well: if something can go wrong, this is exactly what will happen! In this article, we will look at the pitfalls and the right approach to error handling in JS. And finally, let's talk about asynchronous code and Ajax.
I believe that the JS event paradigm adds a certain wealth to the language. I like to present the browser as an event-driven machine, including bugs. In fact, an error is the non-occurrence of some event, although someone will not agree with this. If this statement seems strange to you, then fasten your seat belts, this trip will be unusual for you.
All examples will be considered in relation to client-side JavaScript. The basis of the story went to the ideas voiced in the article "
Exceptional event handling in JavaScript ." The name can be rephrased like this: "When an exception occurs, JS checks for the presence of a handler in the call stack." If you are unfamiliar with basic concepts, I recommend reading this article first. Here we will consider the issue more deeply, not limited to simple requirements for exception handling. So the next time you again get a
try...catch
, then you will approach it with an eye to it.
')
Demo code
The code used for the examples can be downloaded from
GitHub , it represents such a page:

When you click on each of the buttons, a “bomb” explodes, simulating a
TypeError
exception. The following is the definition of this module from the unit test.
function error() { var foo = {}; return foo.bar(); }
First, the function declares an empty
foo
object. Note that there is no
bar()
definition anywhere. Let's now see how our bomb explodes when the unit test runs.
it('throws a TypeError', function () { should.throws(target, TypeError); });
The test is written in
Mocha using test assertions from
should.js . Mocha acts as a test performer, and should.js acts as a library of statements. If you have not yet encountered these test APIs, then you can safely study them. Test execution begins with
it('description')
, and ends with a successful or unsuccessful completion in
should
. Run can be done directly on the server, without using a browser. I recommend not to neglect testing, because it allows you to prove the key ideas of pure JavaScript.
So,
error()
first defines an empty object, and then tries to access the method. But since
bar()
inside the object does not exist, an exception occurs. And this can happen to everyone if you use a dynamic language like JavaScript!
Bad processing
Consider an example of incorrect error handling. I tied the launch handler to a button click. Here is what it looks like in unit tests:
function badHandler(fn) { try { return fn(); } catch (e) { } return null; }
As a dependency, the handler receives a callback
fn
. This dependency is then called from within the handler function. Unit tests demonstrate its use:
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, in the event of a problem, this strange handler returns
null
. A callback
fn()
may indicate either a normal method or a “bomb”. Continuation of a 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 is wrong with getting just
null
? This leaves us in the dark about the cause of the error, does not provide any useful information. Such an approach — stopping execution without explicit notification — can be the cause of incorrect decisions from the point of view of UX that could lead to data corruption. You can kill a few hours for debugging, while losing sight of the
try...catch
. The handler above just swallows errors in the code and pretends that everything is in order. This rolls in companies that are not too concerned about the high quality of the code. But remember that hiding errors in the future is fraught with large time losses for debugging. In a multi-layered product with deep call stacks, it is almost impossible to find the root of the problem. There are a number of situations where it makes sense to use the hidden
try...catch
, but in error handling this is best avoided.
If you use stop execution without explicit notification, then in the end you will want to approach error handling more intelligently. And JavaScript allows for a more elegant approach.
Curve processing
Go ahead. Now it's time to look at the error handler curve. Here we will not touch on the use of DOM, the essence is the same as in the previous part. The curve handler differs from the bad only in the way the exception is handled:
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); });
If you compare it with a bad handler, it is definitely better. The exception causes the call stack to “pop up”. Here, I like the fact that errors will
unwind the stack , and this is extremely useful for debugging. When an exception occurs, the interpreter will go up the stack in search of another handler. This gives us plenty of opportunity to work with errors at the very top of the call stack. But since this is a crooked handler, the original error is simply lost. You have to go back down the stack, trying to find the original exception. Well at least that we know about the existence of a problem that has thrown an exception.
The harm from a crooked handler is less, but the code still turns out with a ghost. Let's see if the browser has an appropriate ace in the hole for this.
Stack rollback
You can wind off exceptions in one way - by placing
try...catch
at the top of the call stack. For example:
function main(bomb) { try { bomb(); } catch (e) {
But with us, the browser is event driven, remember? And exceptions in JavaScript are the same full events. Therefore, in this case, the interpreter interrupts the execution of the current context and produces an unwind. We can use the global
onerror
event
onerror
, and it will look something like this:
window.addEventListener('error', function (e) { var error = e.error; console.log(error); });
This handler can catch the error in any executable context. That is, any error can cause an error event (Error event). The caveat here is that all error handling is localized in one place in the code - in the event handler. As in the case of any other events, you can create chains of handlers to handle specific errors. And if you adhere to the principles of
SOLID , you can ask each error handler its own specialization. You can register handlers at any time, the interpreter will run as many handlers in a loop as needed. At the same time, you can rid your codebase of
try...catch
blocks, which will only be useful for debugging. That is, the point is to approach error handling in JS as well as event handling.
Now that we can unwind the stack with the help of global handlers, what will we do with this treasure?
Stack capture
The call stack is an incredibly useful tool for solving problems. Not least because the browser provides information as is, out of the box. Of course, the
stack property in the error object is not standard, but it is consistently available in the most recent browser versions.
This allows us to do such cool things as logging 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); });
Perhaps, in the above code, this is not striking, but such an event handler will work in parallel with the one above. Since each handler performs any one task, we can follow the
DRY principle when writing code.
I like how these messages are caught on the server.

This is a screenshot of a message from Firefox Developer Edition 46. Please note that due to the correct error handling, there is nothing superfluous here, everything is brief and to the point. And do not hide mistakes! Just look at the message, and it becomes immediately clear who and where threw the exception. Such transparency is extremely useful when debugging front-end code. Such messages can be stored in a persistent store for future analysis in order to better understand the situations in which errors occur. In general, do not underestimate the possibilities of the call stack, including for debugging needs.
Asynchronous processing
JavaScript extracts asynchronous code from the current executable context. This means that there is a problem with
try...catch
expressions like the one below.
function asyncHandler(fn) { try { setTimeout(function () { fn(); }, 1); } catch (e) { } }
The development of events according to the modular version:
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 exception check handler in the handler. Notice that there is an unhandled exception, despite the presence of a block of code around the wonderful
try...catch
. Unfortunately,
try...catch
expressions work only with a single executable context. And by the time the exception was thrown, the interpreter had already moved to another part of the code, left
try...catch
. Exactly the same situation arises with Ajax calls.
Here we have two ways. The first is to catch an exception inside an asynchronous callback:
setTimeout(function () { try { fn(); } catch (e) {
This is quite a working option, but there are a lot of things that can be improved. First,
try...catch
blocks are scattered everywhere - a tribute to the programming of the 1970s. Secondly,
the V8 engine does not
use these blocks too well
inside functions , so developers recommend placing
try...catch
on top of the call stack.
So what should we do? I did not just mention that global error handlers work with any executable context. If such a handler subscribe to a window.onerror event, then nothing else is needed! You immediately begin to follow the principles of DRY and SOLID.
Below is an example of a report sent to the server by an exception handler. If you run the demo, then this report may be slightly different, depending on the browser used.

This handler even reports that the error is related to asynchronous code, more precisely with the
setTimeout()
handler. Right tale!
Conclusion
There are at least two basic approaches to error handling. The first is when you ignore errors, stopping performance without notice. The second is when you immediately receive information about the error and wind off until the moment it occurs. I think everyone knows which of these approaches is better and why. In short: do not hide the problem. No one will blame you for possible failures in the program. It is perfectly acceptable to stop execution, roll back the state and give the user a new attempt. The world is imperfect, so it is important to give a second chance. Mistakes are inevitable, and only how you deal with them matters.