You add the final semicolon to a brand new piece of asynchronous code. Save,
run, and… nothing happens. Like, literally nothing. No errors, no output.
The code just stopped execution midway through. You spend the next five
console.log and a manual binary search to figure out where your
code died. Unhappiness ensues.
- Your async code swallows an important error with no output, in production.
- Your callbacks sometimes mysteriously get run twice.
- Your async code just generally makes you feel sad inside.
These rules will help you out.
Note: Do you use promises? That’s great! I use Async.js though, so this article is probably not for you.
Rule 1: Use consistent naming
Choose a name for the callback variable, and stick with it. Make it short,
since you’ll be typing it quite often. I use
Choose a name for the error that a callback receives. I use
err, and I think
that’s pretty standard.
A quick example:
Easy, but important: consistency is crucial for being able to scan code quickly and accurately.
Rule 2: Async functions must return immediately after using the callback
In my experience, using the callback earlier makes it easy to use it twice by
accident. It also makes it more difficult to scan the function for exit
points. An “async function”, for the purpose of this article, is any function
that accepts a callback
cb as an argument.
Rule 3: Async functions must always use the callback before returning
If an asynchronous function exits before using the callback, execution will
stop and nothing will happen next. This is always undesirable. If something
has gone wrong, pass the callback an
Error to help with debugging.
Using the callback
There are three ways a function can “use the callback”. The first option is to invoke the callback directly, as above:
Second, the function can pass the callback to another async function:
Finally, the function can call some other asynchronous function, and use
inside that function’s callback. Crucially, the same rules about use of
now apply to the callback’s code:
Putting it all together, an asychronous function might look something like this:
Rule 4: Every callback must start with an error check
Unfortunately, this makes it very easy to ignore errors, and silently swallowed errors can easily add an hour of debugging to my day. To mitigate this, I have a strict rule of always starting my callbacks with an error check:
This will ensure that the error “bubbles up” until it gets handled, just like a
normal synchronous exception. Eventually, the error will hit the top level,
and there will no longer be a
cb to pass the error to. At that point, the
error can be logged or shown to the user.
Rule 5: Callbacks and async functions must not return a value
Let’s start with callbacks. Think back to Rule 2: an async function should
exit immediately after invoking a callback. Clearly, if a callback were to
return a value, it would just be ignored. Usually,
return something shows up
in a callback when code was translated from synchronous to asynchronous: it
often indicates a bug.
Asynchronous functions are a bit different. Here’s a legitimate, simplified example of when returning a value synchronously might seem reasonable:
I’ve found that refactoring the synchronous work into a separate function makes the code much easier to follow:
There’s one exception, however.
The operation pattern
Sometimes, it needs to be possible to cancel the asynchronous operation. The best solution that I’ve found so far involves returning an “operation object”:
Unfortunately, this breaks Rule 5 (and makes code somewhat harder to follow), so I’d be interested if you have a simpler alternative.
These five rules have made my work with callbacks massively easier – I can’t imagine working without them. Hopefully, they can work well for you too!
- Use consistent naming
- Async functions must return immediately after using the callback
- Async functions must always use the callback before returning
- Every callback must start with an error check
- Callbacks and async functions must not return a value
Additionally, be sure to familiarize yourself with
Async.js. I only use