Like many modern languages, JavaScript allows you to throw exceptions and catch them in a try/catch block. If uncaught, most environments will give you a helpful stack trace. For example, this code will throw an exception because ’{’ is invalid JSON:
EventModel/stackTrace.js | |
| function JSONToObject(jsonStr) { |
| return JSON.parse(jsonStr); |
| } |
| var obj = JSONToObject('{'); |
<= | SyntaxError: Unexpected end of input |
| at Object.parse (native) |
| at JSONToObject (/AsyncJS/stackTrace.js:2:15) |
| at Object.<anonymous> (/AsyncJS/stackTrace.js:4:11) |
The stack trace tells us not only where the error was thrown from but also where the original mistake was made: line 4. Unfortunately, tracking down the causes of async errors isn’t as straightforward. In this section, we’ll see why throw is rarely the right tool for handling errors in callbacks and how async APIs are designed around this limitation.
What happens when we throw an error from an async callback? Let’s run a test.
EventModel/nestedErrors.js | |
| setTimeout(function A() { |
| setTimeout(function B() { |
| setTimeout(function C() { |
| throw new Error('Something terrible has happened!'); |
| }, 0); |
| }, 0); |
| }, 0); |
The result of this application is an extraordinarily short stack trace.
<= | Error: Something terrible has happened! |
| at Timer.C (/AsyncJS/nestedErrors.js:4:13) |
Wait a minute—what happened to A and B? Why aren’t they in the stack trace? Well, because they weren’t on the stack when C ran. Each of the three functions was run directly from the event queue.
For the same reason, we can’t catch errors thrown from async callbacks with a try/catch block. Here’s a demonstration:
EventModel/asyncTry.js | |
| try { |
| setTimeout(function() { |
| throw new Error('Catch me if you can!'); |
| }, 0); |
| } catch (e) { |
| console.error(e); |
| } |
Do you see the problem here? Our try/catch block will catch only those errors that occur within the setTimeout function itself. Since setTimeout runs its callback asynchronously, even when the timeout is 0, the error it throws will go straight to our application’s uncaught exception handler (see Handling Uncaught Exceptions).
In general, putting a try/catch block around a function that takes an async callback is pointless. (The exception is when the async function does something synchronous and error-prone as well. Node’s fs.watch(file, callback), for example, will throw an error if the target file doesn’t exist.) That’s why callbacks in Node.js almost always take an error as their first argument, allowing the callback to decide how to handle it. For example, this Node app tries to read a file asynchronously and logs any error (such as the file not existing):
EventModel/readFile.js | |
| var fs = require('fs'); |
| fs.readFile('fhgwgdz.txt', function(err, data) { |
| if (err) { |
| return console.error(err); |
| }; |
| console.log(data.toString('utf8')); |
| }); |
Client-side JavaScript libraries are less consistent, but the most common pattern is for there to be separate callbacks for success and failure. jQuery’s Ajax methods follow this pattern.
| $.get('/data', { |
| success: successHandler, |
| failure: failureHandler |
| }); |
No matter what the API looks like, always remember that you can handle async errors only from within a callback. As an async Yoda might say, “Do, or do not. There is no try.”
When we throw an exception from a callback, it’s up to whomever calls the callback to catch it. But what if the exception is never caught? At that point, different JavaScript environments play by different rules….
Modern browsers show uncaught exceptions in the developer console and then return to the event queue. You can modify this behavior by attaching a handler to window.onerror. If the handler returns true, it’ll prevent the browser’s default error-handling behavior.
| window.onerror = function(err) { |
| return true; // ignore all errors completely |
| }; |
In production, you might want to consider a JavaScript error-handling service, such as Errorception.[22] Errorception provides a ready-made window.onerror handler that reports all uncaught exceptions to their server, which can then send you notifications.
Node’s analog to window.onerror is the process object’s uncaughtException event. Normally, a Node app will exit immediately on an uncaught exception. But as long as at least one uncaughtException handler exists, the app will simply return to the event queue.
| process.on('uncaughtException', function(err) { |
| console.error(err); // shutdown averted! |
| }); |
However, as of Node 0.8.4, uncaughtException is deprecated. According to the docs,[23]
uncaughtException is a very crude mechanism for exception handling and may be removed in the future…
Don’t use it, use domains instead.
What are domains? you ask. Domains are evented objects that convert throws into ’error’ events. (We’ll talk more about evented objects in Chapter 2, Distributing Events.) Here’s an example:
EventModel/domainThrow.js | |
| var myDomain = require('domain').create(); |
| myDomain.run(function() { |
| setTimeout(function() { |
| throw new Error('Listen to me!') |
| }, 50); |
| }); |
| |
| myDomain.on('error', function(err) { |
| console.log('Error ignored!'); |
| }); |
The throw from the timeout event simply triggers the domain’s error handler.
<= | Error ignored! |
Magical, isn’t it? Domains make throw much more palatable. Unfortunately, they’re available only in Node 0.8+, and as of this writing, they’re still considered an experimental feature. For more information, see the Node docs.[24]
Whether you’re in the browser or on the server, global exception handlers should be seen as a measure of last resort. Use them only for debugging.
When you’re given an error, the easiest thing to do with it is to throw it. In Node code, you’ll often see callbacks that look like this:
| function(err) { |
| if (err) throw err; |
| // ... |
| } |
We’ll use this idiom frequently in Chapter 4, Flow Control with Async.js. But in a production app, allowing routine exceptions and fatal errors alike to bubble up to the global handler is unacceptable. throw in a callback is a JavaScripter’s way of saying, “I don’t want to think about this right now.”
What about throwing exceptions that you know will be caught? That’s an equally thorny area. In 2011, Isaac Schlueter (creator of npm and current head of Node development) argued that try/catch is an anti-pattern.[25]
Try/catch is goto wrapped in pretty braces. There’s no way to continue where you left off, once the error is handled. What’s worse, in the code that throws, you have no idea where you’re jumping to. When you return an error code, you are fulfilling a contract. When you throw, you’re saying, “I know I was talking to you, but I’m going to jump over you now and talk to your boss instead.” It’s rude. If it’s not an emergency, don’t do that; if it is an emergency, then we should crash.
Schlueter advocated using throws purely as assert-like constructs, a way of bringing applications to a halt when they’re doing something completely unexpected. The Node community has largely followed this recommendation, though that may change with the emergence of domains.
So, what’s the current best practice for handling async errors? I suggest heeding Schlueter’s advice: if you want your whole application to stop, go ahead and use throw. Otherwise, give some thought as to how the error should be handled. Do you want to show the user an error message? Retry the request? Sing “Daisy Bell”? Then handle it like that, as close to the source as possible.
18.118.217.168