JavaScript Async Patterns — a Progression: Part 1, Callbacks

Making asynchronous code easier to reason about.

Todd Zebert
5 min readMar 8, 2017

This is the first of a multi-part series on JS async patterns. We’ll be starting with the fundamentals — callbacks — and progression through various concepts, finally to async / await, as the series unfolds. Each part will cover chained, “all” and “race” patterns, and more.

Callbacks are just functions provided to another function to be called when ready/needed. The basic pattern is very common in JavaScript because it’s been around since the beginning, is broadly understood, and many times it’s quite sufficient.

Note that the code here is illustrative to explain some concepts and does not include “reject”, error checking, etc and is not production appropriate.

We’re going to simulate a hypothetical social network page load — a bit contrived at times but the various steps will illustrate the interaction between various async relationships. But first, some “helper functions” that aren’t critical to understanding.

This is pretty obvious

This next bit of code deserves a little bit of explanation: it “simulates” an async function and passes either res directly or the result of res() to the callback function, which in this simulation just waits a random amount of time:

Simple Nested Callbacks

Now, onto some more interesting code. Using asyncSim as a base, let’s create some idiomatic functions through partial application:

  • getAuth would authorize a user’s login
  • getProfile would return data and settings describing the user, based on the login authorization
  • getFeed would return a list of posts, based on the profile

All getFeed does is return an random length array of random numbers, representing the ID’s of the posts the user should see.

Now we’re to the heart of the callback interaction. Since each async call in our example depends on the result of the prior async call, we get this nested structure — often, and rightly — referred to as “callback hell”. It may not seem too bad at this point but consider we have no business logic, no error checking, no reject code, only 3 contingent async call dependencies, etc.

Each step (getAuth, etc.) adds to the data object the res (result) of the previous step. This is illustrative only as a real application would have to act on the actual result of the previous call.

A sample output object of this would be:

auth : "Authorized"
feed: Array[2]
0: 16
1: 88
2: 53
id: 4
profile: "Profile"

You can see it working here:

The all Pattern

Sometimes the relationships between asynchronous events is not linearly dependent; sometimes you need all of one set of calls to complete before returning a result.

Building on the code from before, we have an expanded set of idiomatic functions:

  • getPost is our example to be wrapped in all, and would return a post object (which is just the post ID modified, for sake of simplicity) for a post ID.

getPost is bit different that previous functions in that it forms a closure so it can use the post ID in the callback, to modify it (simply + 100) and return it as the post object. I’m “cheating” a bit with the assignment and the return, so as not to let added complexity detract from the async discussion.

In order to make “all” work we have to introduce some new code. Here’s some minimal code to support all :

  • First we save the length of the entries — which is an array of callbacks that “all” must “resolve”/finish successfully — as finishers.
  • The next portion allCB() creates another closure that wraps a callback — it either ends with a false (reject) on callback failure, or decrements the count of entries, and if that makes it to 0 (meaning all callback entries have completed successfully) it end with a resolve (a non-false, simply in this case).
  • Finally, the entries array is mapped over, with each callback being passed to the callback wrapper allCB.

Now let’s see what our main nested callback w/”all” looks like. It’s the same as last time except for adding lines 10–14. Since all takes an array of callbacks, we create it in line 10 by mapping over data.feed that was passed in from the previous callback. We then call our new all function with the array, and of course it’s success/resolve callback.

A sample output object of this would be:

auth: "Authorized"
feed: Array
0: 28
1: 96
2: 17
id: 4
posts: Array
17: 117
28: 128
196: 196
profile: "Profile"

You can see it working here:

You’ll want to check the browser’s console for output as codepen’s console likes to print undefined many times in the “sparse” array.

The race Pattern

Like the “all” pattern, “race” also takes an array of callbacks, but only the winning (first) result counts — if it’s resolved, it’s passed onto the overall next callback. But if the winner is a reject/fail, then the whole set of requests is a fail. In “all” there’s no second place (or third, etc) — they just don’t matter at all.

Our final expanded set of idiomatic functions:

  • getAd would return the first ad to be supplied to be placed on the page. For the sake of our example here we’re going to imagine we have a number of ad platforms that will supply ads, but we only care about the first one, and we’ll run with that. (This is a bit contrived because if the first to respond is a reject, we’d have to pass that failure/reject on — whereas you’d probably more reasonably just take the first successful completion, or render the page even if all ads failed.)

getAd definitely needs some explaining. Oh sure, I could have defined getAd1 , getAd2 , etc. functions but this allows unlimited generation of mock ad functions. So, it’s an IIFE that creates a closure (so the counter i is maintained) that will return the same ‘ol asyncSim call we’ve seen before. Having to invoke the returned function (seen below) parallel’s the invocation of getPost function in the last example.

Like with “all”, in order to make “race” work we have to introduce some minimal new code:

  • once() is conceptually the same as _.once() in that it only does something the first time called. In this case through the result passed in (resolve or reject) determines what that something is.
  • race() wraps the passed in callback in once . Then it maps through the array of entries , invoking each one, passing in data and the wrapped callback.

Now let’s see how hellish this callback nesting really is. Again, it’s just like our last code up until line 13 where we call race with an array of ad providers (here arbitrarily 3), along with our callback, of course.

A sample output object of this would be:

ad: "Ad#2"
auth: "Authorized"
feed: Array
0: 62
1: 64
id: 4
posts: Array
162: 162
164: 164
profile: "Profile"

And you can try it here:

Avoiding Nested Callback Hell

We can “flatten” that nested structure—which is advisable — but it comes with it’s own price of naming all the anonymous functions. And in a proper implementation, you’d also need the corresponding 5 reject functions; as much as that seems, a nested callback structure of all 10 would be completely unreasonable.

Other async Patterns

There’s other things to consider, like how to properly handle “reject”, time-outs, retries, cancelation, etc. … and while interesting, they make the code much more complicated, and it’ll detract from getting to the point. If your code gets that complicated you’ll probably want to use a library such as async.

The next in our progression will go through these same steps using promises, including proper reject.

Thanks for reading and I hope this was helpful; leave a comment, a like, or a share, thanks!

--

--

Todd Zebert

IT Sherpa: CTO, Founder, Fullstack Dev, Drupal & Wordpress Mason. Crushing on Angular & Node. Maker. Charitable, curious, caffeinated single dad.