Domains were added to Node.js in 0.8, but their use and workings have been a relative mystery. In short, domains are a structured way of reacting to uncaught exceptions; for example, when creating an HTTP server, you can use domains to send 500 errors when exceptions occur instead of crashing your server. This talk will go over what domains are, how to use them, and some of the subtleties behind how they work.
7. Before domains: process.on('uncaughtException', …)
• Instead of crashing, you could listen for “about to crash,” and do
something about it.
• But: how do you keep track of what caused the crash?
• Concretely: how can you return a 500 error to the right request?
• Domains are supposed to help with this… somehow.
@DOMENIC
8. Build your own domains: uncaught throws
• Before initiating some asynchronous work:
• process.pseudoDomain = new EventEmitter();
• After initiating the async work:
• process.pseudoDomain = null;
• Inside your 'uncaughtException' handler:
• if (process.pseudoDomain) process.pseudoDomain.emit('error', err);
• Now, in the pseudo-domain’s 'error' handler, you know the cause!
@DOMENIC
9. Build your own domains: unheard 'error's
• When creating an event emitter:
• ee.pseudoDomain = new EventEmitter();
• ee.on('error', function (err) {
if (EventEmitter.listenerCount(this, 'error') === 1) {
this.pseudoDomain.emit('error', err);
}
});
• Now, in the pseudo-domain’s 'error' handler, you know the cause!
@DOMENIC
10. Build your own domains: callback err params
• function bindToDomain(domain, cb) {
return function (err, result) {
if (err) return domain.emit('error', err);
cb(null, result);
};
}
• Every time you use a callback:
• var pseudoDomain = new EventEmitter();
• doAsyncThing(bindToDomain(pseudoDomain, function (result) { … });
• Now, in the pseudo-domain’s 'error' handler, you know the cause!
@DOMENIC
11. The key feature: automatic aggregation
• This is pretty useless if you have to create a domain for every async
operation.You might as well keep track of things yourself.
• But what if we assigned a single domain to a whole bunch of async
operations?
• Say, every async operation involved in a single HTTP
request/response cycle?
• What if we had hooks into every asynchronous function and every
event emitter in Node.js, to automatically associate with an
“active” domain?
@DOMENIC
13. But … how!?
To understand how domains hook in to all that, we’re going
to need to take a journey into the heart of Node.js.
@DOMENIC
14. MakeCallback
node.cc line 1001:
Handle<Value> MakeCallback(const Handle<Object> object,
const Handle<Function> callback,
int argc, Handle<value> argv[]) {
⋮
TryCatch try_catch;
Local<Value> ret = callback->Call(object, argc, argv);
if (try_catch.HasCaught()) {
FatalException(try_catch);
⋮
@DOMENIC
15. Domain basics
• var d = require('domain').create();
• There’s a globally active current domain:
• process.domain === require('domain').active
• (There’s actually a stack, so you can recursively enter domains, but whatevs.)
• d.enter() makes d the current domain.
• d.exit() makes d inactive.
• d.run(func) is shorthand for d.enter(); func(); d.exit().
@DOMENIC
16. Effects of having an active domain
• MakeCallback is most directly influenced
• process.domain.enter() and process.domain.exit() wrap the previous code
• setImmediate, setTimeout, setInterval, process.nextTick
• record the active domain and attach it to the callback (1, 2, 3, 4)
• when the callback is triggered, wrap it with enter() and exit() (1, 2/3, 4)
• new EventEmitter()
• records the active domain and attach it to the emitter
• when any events are emitted, wraps with enter() and exit()
• when an 'error' event is emitted and not handled, gives it to the domain
@DOMENIC
(mmm, yummy global state…)
17. Uncaught exceptions
• Remember FatalException(try_catch)?
• That calls from C++ to process._fatalException (node.js line 222)
• Much like in our pseudo-domains, it hands them off to the active
domain’s 'error' handler.
• Thus all the enter()/exit() wrapping was just establishing context for this
moment: deciding which domain gets the errors.
• If there is an active domain, this behavior replaces
'uncaughtException'.
@DOMENIC
18. That’s cool. Now what?
We know how domains work.
But do we truly know how to use them?
@DOMENIC
19. Stability 2: Unstable
“TheAPI is in the process of settling, but has not yet had sufficient
real-world testing to be considered stable. Backwards-compatibility
will be maintained if reasonable.” (source)
• Don’t use d.dispose().
@DOMENIC
20. APIs to know
• domain.create(), obviously
• d.run(), to enter/exit the domain
• d.add(), for adding event emitters created before the domain
existed into the domain.
• d.bind(), for manually wiring up a callback to a domain.
• d.intercept(), that callback helper from before.
@DOMENIC
21. EventEmitter pitfalls
• Event emitters are bound to a domain on creation. But sometimes
event emitters stick around for a long time.
• Any callbacks given to the event emitter “escape” the active
domain, which is replaced by whatever the active domain was
when the event emitter was created.
• Generally, anything involving a persistent connection or
connection pool will be in trouble, and not able to associate errors
with the currently-active domain.
• https://github.com/domenic/domains-tragedy
@DOMENIC
22. Error recovery
• Domains don’t give you try/catch back.
• By the time you see an error on the domain, it escaped any close-
to-the-source error handling; nobody handled it explicitly.
• You’re probably in an inconsistent state, halfway through a
transaction or with file handles open or who knows what.
• The recommendation is to gracefully return a 500, then shut down
your worker and restart while others in the cluster take over.
@DOMENIC
23. Versus promises
• Domains have grown … organically.
• Promises attempt to solve error handling from first principles, by
giving you back return and throw semantics.
• In practice, this means wrapping every callback in try/catch, much
like MakeCallback, but less pervasively and more intelligently.
• But, they won’t save you completely in Node.js:
• 'error' events are still fatal
• If you step outside of promise-land, e.g. setTimeout, uncaught exceptions
are again dangerous.
@DOMENIC
24. IN CONCLUSION
• Domains work pretty well to tame
async errors in Node.js.
• They do so by hooking into Node.js at
the deepest level.
• They don’t work perfectly, and they
don’t absolve you of cleanup.
• But you should be able to get some
solid wins with minimal extra code.
@DOMENIC
Editor's Notes
Opening story: GothamJS, diving into AngularJS code.
I work here at Lab49!
The first of these, you handle yourself.The second, by design, re-throws the error if there are no listeners for it.The third, since we’re almost always inside a callback, can’t bubble up the call stack; callbacks reset the call stack, so they hit the top of that call stack pretty quickly.—If we were in a browser, those would hit window.onerror.
(On switch back) How can we fix this??
Let’s build our own “domains” to track error sources!
I dug into the source code, and figured out how domains work. This is basically it.
I dug into the source code, and figured out how domains work. This is basically it.
Since we’ve got this working for everything else, might as well try to make callbacks a bit easier to use.
All callbacks you give to Node.js’s code pass through here.Either directly, from C++ code which directly calls MakeCallback to execute your callbackOr indirectly, via a convoluted chain inside src/node.js that ends up in process._makeCallback.
To understand how this gives us the power we need, let’s take a step back and see how the domain object themselves work.
The moment you start using domains all this gets switched out from under you. process._setupDomainUse()BUT: wrapping in enter()/exit() is just the setup. enter() and exit() do literally nothing besides setting the active domain. The magic takes place in our next chapter: