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 minutes using console.log and a manual binary search to figure out where your code died. Unhappiness ensues.

Or maybe:

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 cb.

Choose a name for the error that a callback receives. I use err, and I think that’s pretty standard.

A quick example:

// Callback is always the last argument to an async function.
function readFileAsync(filePath, cb) {
	// read the file
	// then call cb
}

// Let's use our function!
// First argument to the callback is always `err`
readFileAsync('/path/to/file', function (err, result) {
	// ...
});

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.

// BAD
function loadTable(cb) {
	// ...
	cb(); // Oops!  This probably came from a bad copy/paste.

	// ...
	// some more code here
	// ...
}

// GOOD
function loadTable(cb) {
	// ...

	if (isFoo) {
		cb(); // OK: return immediately after
		return;
	}

	// ...
	// some more code here
	// ...

	cb(); // OK: returns (due to end of function) immediately after
}

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.

// BAD
function loadTable(cb) {
	// ...

	if (isFoo) {
		return; // Oops: missed call to cb, code will stop here
	}

	// ...

	// Oops: no call to cb before end of function, code will stop here
}

// GOOD
function loadTable(cb) {
	// ...

	if (isFoo) {
		cb();
		return;
	}

	// ...

	cb();
}

Using the callback

There are three ways a function can “use the callback”. The first option is to invoke the callback directly, as above:

cb(null, result); // pass back a result, if you like

Second, the function can pass the callback to another async function:

uploadResults('/upload-file', results, cb); // `uploadResults` is now responsible for invoking the callback

Finally, the function can call some other asynchronous function, and use cb inside that function’s callback. Crucially, the same rules about use of cb now apply to the callback’s code:

uploadResults('/upload-file', results, function (err, requiresRefresh) {
	if (err) {
		// Use callback immediately before return
		cb(null, {status: 'upload-failed'});
		return;
	}

	if (requiresRefresh) {
		// Use callback immediately before return by passing it to another
		// async function
		refreshResultList(cb);
		return;
	}

	// Use callback immediately before end of function
	cb(null, {status: 'ok'});
});

Putting it all together, an asychronous function might look something like this:

function submitResults(rawData, cb) {
	var results = processData(rawData);

	if (results.length === 0) {
		// invoke callback directly
		cb(new Error('no results from data processing'));
		return;
	}

	// Here, we call an async function just before the end of the function.
	// This qualifies as "using cb", as long as the callback code below also
	// follows the rules.
	uploadResults('/upload-file', results, function (err, requiresRefresh) {
		if (err) {
			// Use callback immediately before return
			cb(null, {status: 'upload-failed'});
			return;
		}

		if (requiresRefresh) {
			// Use callback immediately before return by passing it to another
			// async function
			refreshResultList(cb);
			return;
		}

		// Use callback immediately before end of function
		cb(null, {status: 'ok'});
	});
	// No more code allowed here, since the `uploadResults` statement used cb.
}

Rule 4: Every callback must start with an error check

Asynchronous code, at least in JavaScript, means handling errors yourself. In C, the presence of an error is indicated using a return value. Programming with callbacks is very similar: errors are passed to the callback, which is the asynchronous equivalent of a “return value”. Every callback is responsible for checking for and handling errors that it receives.

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:

function loadPage(cb) {
	loadStuff(function (err, stuff) {
		if (err) {
			// Optional: we can handle the error here if we recognize it
			if (err instanceof WhateverError) {
				// ... handle the error here ...
				// use `cb` before returning
				return;
			}

			// Since we can't handle it, just pass it up to the callback.
			// This will cause the error to "bubble up" to the function that
			// called `loadPage`.
			cb(err);
			return;
		}

		// OK, now we can actually do stuff.
		// ...
		// use `cb` before finishing
	});
}

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:

// Example path: '/2015/01/01/document-title-here'
function loadDocument(path, cb) {
	// Get the date out of the path
	var docDate = ...;

	// Get the title out of the path
	var docTitle = ...;

	var docInfo = {
		date: docDate,
		title: docTitle,
	};

	// ... some more stuff ...
	loadFile(path, extraArg, cb);

	// Return the date and title right away to show while things are loading
	return docInfo;
}

I’ve found that refactoring the synchronous work into a separate function makes the code much easier to follow:

// Example path: '/2015/01/01/document-title-here'
function getDocumentInfo(path) {
	// Get the date out of the path
	var docDate = ...;

	// Get the title out of the path
	var docTitle = ...;

	return {
		date: docDate,
		title: docTitle,
	};
}

function loadDocument(path, cb) {
	// ... some more stuff ...
	loadFile(path, extraArg, cb);
}

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”:

function startLongOperation(arg1, arg2, cb) {
	var isCancelled = false;

	// ...
	// do the actual work
	// check isCancelled periodically, and stop if set to true
	// `cb` must not be called once `cancel` is called
	// ...

	return {
		cancel: function () {
			isCancelled = true;
		},
	};
}

var longOp = startLongOperation(arg1, arg2, function (err) {
	// ...
});

// later...
longOp.cancel()

Unfortunately, this breaks Rule 5 (and makes code somewhat harder to follow), so I’d be interested if you have a simpler alternative.

That’s it.

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!

  1. Use consistent naming
  2. Async functions must return immediately after using the callback
  3. Async functions must always use the callback before returning
  4. Every callback must start with an error check
  5. Callbacks and async functions must not return a value

Additionally, be sure to familiarize yourself with Async.js. I only use series, parallel, map, each, and whilst.