- Mastering Node.js
- Sandro Pasquali
- 1210字
- 2021-07-21 18:17:14
Callbacks and errors
Members of the Node community develop new packages and projects every day. Because of Node's evented nature, callbacks permeate these codebases. We've considered several of the key ways in which events might be queued, dispatched, and handled through the use of callbacks. Let's spend a little time outlining the best practices, in particular about conventions for designing callbacks and handling errors, and discuss some patterns useful when designing complex chains of events and callbacks.
Conventions
Luckily, Node creators agreed upon sane conventions on how to structure callbacks early on. It is important to follow this tradition. Deviation leads to surprises, sometimes very bad surprises, and in general to do so automatically makes an API awkward, a characteristic other developers will rapidly tire of.
One is either returning a function result by executing a callback, handling the arguments received by a callback, or designing the signature for a callback within your API. Whichever situation is being considered, one should follow the convention relevant to that case:
- The first argument returned to a callback function is any error message, preferably in the form of an error object. If no error is to be reported, this slot should contain a
null
value. - When passing a callback to a function it should be assigned the last slot of the function signature. APIs should be consistently designed this way.
- Any number of arguments may exist between the error and the callback slots.
Know your errors
It is excellent that the Node community has automatically adopted a convention that compels developers to be diligent and report errors. However, what does one do with errors once they are received?
It is generally a very good idea to centralize error handling in a program. Often, a custom error handling system will be designed, which may send messages to clients, add to a log, and so on. Sometimes it is best to throw
errors, halting the process.
Node provides more advanced tools for error handling. In particular, Node's domain
system helps with a problem that evented systems have: how can a stack trace be generated if the full route of a call has been obliterated as it jumped from callback to callback?
The goal of domain
is simple: fence and label an execution context such that all events that occur within it are identified as such, allowing more informative stack traces. By creating several different domains for each significant segment of your program, a chain of errors can be properly understood.
Additionally, this provides a way to catch errors and handle them, rather than allowing your entire Node process to collapse.
In the following example we're going to create two domains: appDomain
and fsDomain
. The goal is to be able to trace which part of our application is in an error state:
var domain = require("domain"); var fs = require("fs"); var fsDomain = domain.create(); fsDomain.on("error", function(err) { console.error("FS error", err); }); var appDomain = domain.create(); appDomain.on('error', function(err) { console.log("APP error", err); });
We now wrap the main program in appDomain
, and the filesystem calls in fsDomain
. We then create an error in fsDomain
by trying to open a non-existent file:
appDomain.run(function() { process.nextTick(function() { fsDomain.run(function() { fs.open('no_file_here', 'r', function(err, fd) { if(err) { throw err; } appDomain.dispose(); }); }); }); });
When the preceding code executes, something resembling this should be echoed to the terminal:
FS error { [Error: ENOENT, open 'non-existent file'] errno: 34, code: 'ENOENT', path: 'non-existent file', domain: { domain: null, _events: { error: [Function] }, _maxListeners: 10, members: [] }, domainThrown: true }
Now let's create an error in appDomain
by adding this code, which will produce a reference error (as no b
is defined):
appDomain.run(function() { a = b; process.nextTick(function() { ...
An error similar to that in the precious code should be generated and reported by appDomain
.
Notice the command appDomain.dispose
. As maintaining these error contexts will consume some memory, it is best to dispose of them when no longer needed—after the code they contain has successfully executed, for example. We'll learn more advanced uses of this tool as we progress into more complex territories.
As an application grows in complexity it will become more and more useful to be able to trap errors and handle them properly, perhaps restarting only one part of an application when it fails rather than the entire system.
Building pyramids
Simplifying control flows has been a concern of the Node community since the very beginning of the project. Indeed, this potential criticism was one of the very first anticipated by Ryan Dahl, who discussed it at length during the talk in which he introduced Node to the JavaScript developer community.
Because deferred code execution often requires the nesting of callbacks within callbacks a Node program can sometimes begin to resemble a sideways pyramid, also known as "The Pyramid of Doom".
Accordingly, there are several Node packages available which take the problem on, employing strategies as varied as futures, fibers, even C++ modules exposing system threads directly. The reader is encouraged to experiment with these:

A more interesting general point is available here for us to consider regarding API choices in Node. Dahl might have reacted to this criticism by, for example, making one of the listed libraries part of Node's core, or indeed changing the entire way JavaScript is written. Instead, it was left to the community to determine the best practices, and to write the relevant packages. This is the Node way.
Considerations
Any developer is regularly making decisions with a far-reaching impact. It is very hard to predict all the possible consequences resulting from a new bit of code or a new design theory. For this reason, it may be useful to keep the shape of your code simple, and to force yourself to consistently follow the common practices of other Node developers. These are some guidelines you may find useful, as follows:
- Generally, try to aim for shallow code. This type of refactoring is uncommon in non-evented environments—remind yourself of it by regularly re-evaluating entry and exit points, and shared functions.
- Where possible provide a common context for callback re-entry. Closures are very powerful tools in JavaScript, and by extension, Node. As long as the context frame length of the enclosed callbacks is not excessive.
- Name you functions. In addition to being useful in deeply recursive constructs, debugging code is much easier when a stack trace contains distinct function names, as opposed to
anonymous
. - Think hard about priorities. Does the order, in which a given result arrives or a callback is executed, actually matter? Importantly, does it matter in relation to I/O operations? If so, consider
nextTick
andsetImmediate
. - Consider using finite state machines for managing your events. State machines are (surprisingly) under-represented in JavaScript codebases. When a callback re-enters program flow it has likely changed the state of your application, and the issuing of the asynchronous call itself is a likely indicator that state is about to change.
- Mastering Entity Framework Core 2.0
- Learning Apex Programming
- Machine Learning with R Cookbook(Second Edition)
- Web Development with Django Cookbook
- Java:Data Science Made Easy
- Effective Python Penetration Testing
- Microsoft System Center Orchestrator 2012 R2 Essentials
- Redis Essentials
- HDInsight Essentials(Second Edition)
- C++新經典
- Python3.5從零開始學
- Citrix XenServer企業運維實戰
- Canvas Cookbook
- 寫給大家看的Midjourney設計書
- Java7程序設計入門經典