Learn More About Different Async Patterns in JS - ByteScout
Announcement
Our ByteScout SDK products are sunsetting as we focus on expanding new solutions.
Learn More Open modal
Close modal
Announcement Important Update
ByteScout SDK Sunsetting Notice
Our ByteScout SDK products are sunsetting as we focus on our new & improved solutions. Thank you for being part of our journey, and we look forward to supporting you in this next chapter!
  • Home
  • /
  • Blog
  • /
  • Learn More About Different Async Patterns in JS

Learn More About Different Async Patterns in JS

Background: what’s async, at the start?

Async programming has been made popular because of JavaScript, but it exists in fact in many languages. In pure theory, you can even do async programming in C if you feel really adventurous.

But first, what’s async programming? Well, it’s a way to deal with operations that can take time. Opening a file, doing a network request, wait for an input from the user, all of these operations take time. Generally, during this time, your JavaScript code is doing nothing except waiting for anything to happen. But this is generally not reflected in the code. There is not a magic “wait” keyword in JavaScript, so how to say, in code, “at that point I’m ready to wait for events to happen”?

To give you an example and better understand this article, we’ll take the example of a quiz app. The app shows a question, and the user can type an answer in a text field. The right answer is stored on a server so we need to fetch it from the network. The user is right if the answer from the network is equal to the answer he typed.

In other languages, it’s generally a special function call that will make you wait. For example, in Python:

# Assume question has been printed earlier
typedAnswer = input(“Answer: ”)
rightAnswer = fetchAnswer()
isUserRight = rightAnswer == typedAnswer

In Python, line 2 will wait “magically” for the user to type the current year (or their dog’s name if they want to crash the Python script). That’s good, but imagine fetchAnswer() is doing a network request. You can’t optimize the program by doing the network request while waiting for the user to type. You have to wait for the user, and then for the request. It’s slower, and you irritate your users. Not good.
JavaScript solves the problem with async programming. Basically, you say “once the user has finished typing, call this function” and Node.JS or your browser will call back that function when the user is done. These functions are called “callbacks” for this reason.

var rightAnswer, typedAnswer;

textField.addEventListener("keyup", function(textEvent) {
  // Consider the user submits the answer when pressing “Enter”
  if (!isEnterKey(textEvent)) {
    return;
  }

  typedAnswer = textEvent.target.value;
 });

fetchAnswer(function(answer) {
  rightAnswer = answer;
});

You’ll notice it doesn’t stop on line 3. Instead, fetchAnswer will get executed right away and the function at line 4 will be called when the user types few chars in the text field, while the function at line 13 will be called when the network request is answered.

So don’t forget this: in async programming, the lines below a line of code are generally executed right away, while the code lines inside the inner functions are generally executed later. This is the reverse of C, Python etc.

Errors

Programming is often about failures and unexpected cases, and it’s the same for asynchronous programming. The thing is that in JavaScript, you generally deal with failures by throwing and catching exceptions. Async programming is generally more complex, so it doesn’t work exactly that way.

While we’re going to review the async patterns, look carefully about error handling as it’s one of the most important parts of these patterns. This will avoid you a few surprises.

Call Me Maybe: Callbacks

This is the first type of async pattern you meet. And as I just warned, error handling will make it more complex as inside this pattern, there’s in fact 2 patterns competing.
You’ve probably used callbacks without even knowing you did: if you ever used setTimeout, you did write a callback.

setTimeout(function() {
  console.log("I’m called 10 seconds later.");
}, 10000);

This is the simplest. However, you may sometimes give the callback as the first argument, or as the last argument – check documentation carefully as each library is different (and sometimes even a single library can have both conventions).
But what we described above only applies when there is no error handling, or when the async operation can’t fail (which is rare). When there is errors involved, you’ll find at least 2 major conventions. You can’t choose the convention yourself, unless if you write a function accepting callbacks yourself.

Style 1: Success callback, the error callback

This style is maybe the simplest to understand. For each async function, you have to give 2 callbacks: one which will be called upon success, and one which will be called upon failure. For example, if you open a file, under normal conditions it will call the success callback, but if the path is invalid, it will call the error one.
Real example with the browser API to get camera/mic:

navigator.getUserMedia({
  "audio": true,
  "video": false
}, function(stream) {
  // Everything gone well.
  
}, function(err) {
  // Something gone wrong and err contains an exception.
  
});

In try/catch, there is also finally, for code that needs to be executed in both cases. In callback world, you have to make a function (like cleanup()) and call it in both callbacks to ensure this piece of code is executed doesn’t matter how gone your async operation.

In order to make things easier, you can create a function “onError” and give it as a callback. Generally, the error handler for opening the file and writing in the same file is the same, so you really don’t want to write an error callback for each operation you do on the file.

Style 2: errback, popular in Node.JS

You’ll probably meet this style mostly in Node.JS. The concept is pretty different from style 1: there is only one callback as an argument, but the callback’s first argument is err, which describes an error. If everything has gone well, the first argument will be null or undefined. If anything failed, the first argument will contain helpful data to investigate the error (an Error object, or anything meaningful).

dns.lookup("www.google.com", function(err, address) {
  if (err) {
    // Error handling
    console.log("Error: " + err);
    
  } else {
    console.log("Google Addresses: " + JSON.stringify(address));
    
  }
});

This looks pretty different compared to style 1. First, you can implement finally more easily. You just need to put your code below the if/else. Second, for the style of your code, you might have to indent more your code if you use the if/else approach. There is another approach with less indent:

dns.lookup("www.google.com", function(err, address) {
  if (err) {
    // Error handling
    console.log("Error: " + err);
    return;
  }

  console.log("Google Addresses: " + JSON.stringify(address));
  
});

That’s less boilerplate, and you have indents like the style 1 for the success callback.
This style of callback avoids writing function() everywhere. However, it is more error-prone. Notice there is a return on line 5. If you forget this return, you end up having the “success” code running when there’s an error! Same affair if you forget the else in the first example.
Given you write basically hundreds of callbacks like this in a full application, it might be likely you hit that bug one day. So to enjoy quietly this more terse style of coding, you have to be more careful while writing code – but normally, we’re already careful while programming, and this doesn’t prevent bugs anyway.

How to manage concurrent events

So far, I have shown you simple examples. But in a real-world situation, you might easily meet something more complex. You remember the little example at the beginning, with the quiz?
There is a small problem with that example: where to put the answer check? You can’t put it in the fetchAnswer callback, because at that point the user isn’t done typing the answer, and if you can’t in the text field’s callback because the user might have a slow connection (or be a typing contest winner) and the right answer from the network isn’t available yet.

var rightAnswer = null,
    typedAnswer = null;

textField.addEventListener("keyup", function(textEvent) {
  // Consider the user submits the answer when pressing “Enter”
  if (!isEnterKey(textEvent)) {
    return;
  }

  typedAnswer = textEvent.target.value;
  // rightAnswer may still be null at that point.
 });

fetchAnswer(function(answer) {
  rightAnswer = answer;
  // typedAnswer may also be null at that point.
});

That’s right, you don’t know in which order the callbacks will be called. It will depend on user input or network, not of your app. So, you might be tempted to do this:

var rightAnswer = null,
    typedAnswer = null;

textField.addEventListener("keyup", function(textEvent) {
  // Consider the user submits the answer when pressing “Enter”
  if (!isEnterKey(textEvent)) {
    return;
  }

  typedAnswer = textEvent.target.value;

  if (rightAnswer !== null) {
    if (typedAnswer !== rightAnswer) {
      showError("Wrong answer. The right answer is: " + rightAnswer);
    }

    nextQuestion();
  }
 });

fetchAnswer(function(answer) {
  rightAnswer = answer;
  // typedAnswer may also be null at that point.
  
  if (typedAnswer !== null) {
    if (typedAnswer !== rightAnswer) {
        showError("Wrong answer. The right answer is: " + rightAnswer);
    }

    nextQuestion();
  }
});

Enjoy the copy-paste, one of the programmer’s nightmare. Except if you like to code everything twice and don’t mind doing mistakes, you’ll hate this. For this pattern to be good, you’ll prefer something like this:

var rightAnswer = null,
    typedAnswer = null;

function checkAnswer() {
  if (rightAnswer !== null && typedAnswer !== null) {
    if (typedAnswer !== rightAnswer) {
      showError("Wrong answer. The right answer is: " + rightAnswer);
    }

    nextQuestion();
  }
}

textField.addEventListener("keyup", function(textEvent) {
  // Consider the user submits the answer when pressing “Enter”
  if (!isEnterKey(textEvent)) {
    return;
  }

  typedAnswer = textEvent.target.value;

  checkAnswer();
 });

fetchAnswer(function(answer) {
  rightAnswer = answer;
  
  checkAnswer();
});

OK, so yet another function, but it is better, alright?

There are other patterns, I Promise you

One of the other most popular async patterns that even got implemented in the ECMAScript 6 specification and being used in new Web APIs like the Fetch API, Promises are trying to handle differently the error handling than callback does.

In callbacks, error handling is really shown differently compared to synchronous code with try/catch/finally. Promises try to get closer to try/catch/finally to make the async code more look like a synchronous one.

To do so, each async operation has now one object called “Promise”. This object is a manager of async operations. You can tell this manager “when it succeeds, call this function” or “when it fails, call this one”. In Promise world, an async function like fetchAnswer won’t take a function as an argument, it will return this special object and be using that object, you can add your callback.

Let’s assume dnsLookup function is like DNS.lookup from Node.JS, but with promises instead of callbacks.

var dnsPromise = dnsLookup("www.google.com");

dnsPromise.then(function(addresses) {
  console.log("Google addresses: " + JSON.stringify(addresses));
});

This looks a lot like callbacks, alright? Not that much if you look closely.

First thing, you can bind multiple callbacks to the same manager, all of them is going to be called. Even better, if you do:

var dnsPromise = dnsLookup("www.google.com");

// Wait 100 seconds before adding the callback.
setTimeout(function() {
  dnsPromise.then(function(addresses) {
    console.log("Google addresses: " + JSON.stringify(addresses));
  });
}, 100000);

It is going to work. Yes, even on fast connections. It is because the promise will store the value once available, and when someone adds a callback, it will call the callback right away with the stored value.

Moreover, Promises can be chained. Since each Promise object is a manager, the manager can have few goodies to help the programmer transparently, like chaining promises. Example:

// Lookup Google, and then Microsoft.
var dnsPromise = dnsLookup("www.google.com");

dnsPromise.then(function(addresses) {
  console.log("Google addresses: " + JSON.stringify(addresses));

  return dnsLookup("www.microsoft.com");
}).then(function(addresses) {
  console.log("Microsoft addresses: " + JSON.stringify(addresses));  

});

The surprise is that we return a Promise inside the callback. Why are we doing that? When then is called, it stores the callback inside the Promise, but it also returns a new Promise. This new Promise tracks the callback you set in the then function, it will take its return value and will wait for it to complete.

When www.google.com is successfully looked up, the callback is called, it shows the addresses and returns another promise, the one fetching Microsoft. As there’s the new Promise tracking the return value, it will find that our function returned this Microsoft promise, and it will wait for it to complete. Once all completed, the 2nd function in then will take the Microsoft address and display it in the console.
I know this is weird to explain. But that’s pretty logic in fact: the objective is to make chaining easier. On each then, you want to be called only when it’s all done, much like in synchronous code. After all, if you want to set competing Promises, you just need to do it outside the then:

// Lookup Google and Microsoft at the same time.
// Note that Microsoft might be shown before Google.
var googlePromise    = dnsLookup("www.google.com"),
    microsoftPromise = dnsLookup("www.microsoft.com");

googlePromise.then(function(addresses) {
  console.log("Google addresses: " + JSON.stringify(addresses));

});

microsoftPromise.then(function(addresses) {
  console.log("Microsoft addresses: " + JSON.stringify(addresses));  

});

That’s why returning a Promise inside a then function is considered as a signal to say “wait for the returned Promise to complete and give its value”.

There is also error handling. In Promises, there is once again 2 styles, but the most popular is probably this one:

// Lookup Google, and then Microsoft.
var dnsPromise = dnsLookup("www.google.com");

dnsPromise.then(function(addresses) {
  console.log("Google addresses: " + JSON.stringify(addresses));

  return dnsLookup("www.microsoft.com");
}).then(function(addresses) {
  console.log("Microsoft addresses: " + JSON.stringify(addresses));  

}).catch(function(err) {
  console.log("Something wrong happened while looking up Google or Microsoft: " + err);

});

As I said, Promises’ objectives are to look closer to the synchronous code, and this syntax pretty helps. The catch keyword remembers try/catch and makes us easily understand that this function is going to be called in case something goes wrong. But the really nice thing is that catch is called whenever Google or Microsoft fails. You don’t need to call a function in each callback, the manager will understand alone that it should forward this error to catch.

Comparison between the 2 styles

Promises and callbacks might look pretty different, doesn’t matter if we talk about semantics or syntax, it uses exclusively the same thing at the start: functions. Promises just decorated this around a specification and many manager objects that do a lot of stuff, while callbacks look more like basic JavaScript, and most notably, it doesn’t require a lot of prior knowledge to start using callbacks.

Callbacks have a first benefit that Promises can’t match, it is memory pressure. As you know, JavaScript is a garbage collected language, with the runtime automatically cleaning up unused variables.

As Promises creates an object on each async operation, if you do a lot of them (and in a server, you do), each call to a Promise-based function and each call to then/catch will create an object, themselves with multiple properties and so with objects. This requires its share of cleanup upon garbage collection, and so as there are more variables, garbage collections take more time. Callbacks instead get directly the asynchronous value, it does not get stored inside the Promise object, and no Promise object is ever created.

The second benefit of callbacks is its simpler semantics, especially when you don’t chain a lot of events. Anyone knowing basic JavaScript understands that setTimeout is going to call your callback after the duration you set, and knows how to write a code using setTimeout without reading docs. Using Promises without reading docs isn’t a good idea. The semantics are just going to surprise you sooner or later. These different semantics are what’s good in Promises, so you don’t want to take them away. That’s just the price to pay for Promises.

In contrast, the management of Promises allows you to do advanced asynchronous management. You can easily tell “do this, and then that” or “do this and that at the same time”, you can set an error handler on a Promise chain, you can even return Promises or give them as arguments in order to chain your operations in an advanced way. It’s flexible, and the manager objects will basically do the heavy lifting for you.

Another benefit of Promises is that the manager manages also your then function. Say, you try to throw an Error inside a then function, the Promise manager will catch the error and call automatically the error handler of the Promise. As you saw earlier, it also manages the return values of the callback, so that brings convenience to the programmer.

OK, but what about our quiz? Remember, we want to launch the request while the user is typing the answer, but we also need to check the answer only when both events happened. As well, how to modify our fetchAnswer usage to use Promises? And how to convert our textField event handler?

This is the 3rd problem with Promises/callbacks. You generally don’t choose what style of async other functions do and use. If you want to unify and use Promises, you’ll need to create wrappers that make callback-based functions compatible with Promises. This requires more work, even if it might pay off later when writing app code.

Finally, check the compatibility. In order to use ECMAScript 6 Promises, you need ECMAScript 6. I know, that’s pretty expected, but not all browsers support it natively. Otherwise, you’ll have to use a Promise shim. Callbacks are available since the creation of JavaScript, so you don’t need to check if they’re available or not.

Using when.js to help you with Promises

As you’ve seen, it requires more work to use Promises. The good news is that some people wrote libraries to help you in many of the common tasks of async programming.

After all, basic ECMAScript 6 Promises doesn’t feature, for example, a way to do finally, to have something that gets called doesn’t matter if it succeeds or fails. And you need ECMAScript 6.

when.js is there to help with all these tasks. It includes a package to give you a Promise shim so you can use Promises even in the browser or older versions of Node.JS, and it includes a lot of functions to work with Promises.

In order to benefit from when.js benefits, you need to wrap all Promises into when.js given it has its own Promise manager. After all, the library isn’t going to monkey-patch the Promise’s prototype, right?

So, for example, let’s say dnsLookup uses normal Promises from ECMAScript 6, you have to do:

var dnsPromise = when.resolve(dnsLookup("www.google.com"));

And now, dnsPromise will have all features of when.js.

So, you remember our example with the quiz? We couldn’t convert the code to Promises yet because doing so in pure ECMAScript 6 is harder. Let’s see how we can use when.js to make that easier.

First, we need to convert our textField event handler become a Promise function. To do so, our best friend is when.promise. Note there are multiple ways to create Promises in when.js, but we had to choose one.

when.promise works basically like this: it takes a function as an argument. This function is called immediately with 2 parameters: a resolve function and a reject function. when.promise will then return a new Promise. Inside the function, you can use callback-based functions to do your work, and then call resolve() when you’ve got the final value you want to give to then functions, or reject() when something went wrong. The Promise returned earlier will be updated accordingly. So, for getting the user’s answer:

function getUserAnswer() {
  // Create a Promise which will returns the user's answer as soon the user
  // typed Enter key.
  return when.promise(function(resolve, reject) {
    textField.addEventListener("keyup", function textListener(textEvent) {
      var typedAnswer = "";

      // Consider the user submits the answer when pressing “Enter”
      if (!isEnterKey(textEvent)) {
        return;
      }

      typedAnswer = textEvent.target.value;

      // Promises only resolves once, so let's remove the event listener once
      // resolved.
      textField.removeEventListener("keyup", textListener, false);
      resolve(typedAnswer);

     }, false);
  });
}

There you go! You have now a getUserAnswer function that returns a Promise! Our only last problem now is, how to check the answer only when getUserAnswer & fetchAnswer have done their work?

The function we need for that is when.all. This function takes an Array of Promises and will wait for all Promises to succeed or fail. Once done and if everything has gone right, it will call the functions with an Array as the argument of the function. If one of them failed, it will call the catch handler. Let’s bring all the pieces all together for the quiz:

function getUserAnswer() {
  // Create a Promise which will returns the user's answer as soon the user
  // typed Enter key.
  return when.promise(function(resolve, reject) {
    textField.addEventListener("keyup", function textListener(textEvent) {
      var typedAnswer = "";

      // Consider the user submits the answer when pressing “Enter”
      if (!isEnterKey(textEvent)) {
        return;
      }

      typedAnswer = textEvent.target.value;

      // Promises only resolves once, so let's remove the event listener once
      // resolved.
      textField.removeEventListener("keyup", textListener, false);
      resolve(typedAnswer);

     }, false);
  });
}

// fetchAnswer might use another type of Promises.
var typedAnswerPromise = getUserAnswer();
    rightAnswerPromise = when.resolve(fetchAnswer());

when.all([typedAnswerPromise, rightAnswerPromise]).then(function(values) {
  var typedAnswer = values[0],
      rightAnswer = values[1];

  if (typedAnswer !== rightAnswer) {
    showError("Wrong answer. The right answer is: " + rightAnswer);
  }

  nextQuestion();

}).catch(function(err) {
  showError("Technology messed up. Sorry.\n" + err);

}).finally(function() {
  // Clear the user's answer text field, in any case.
  clearUserAnswer();

});

Wo-hoo! We got our quiz converted to Promises. And notice a little goodie of when.js library: you can use finally with when.js promises. This can be pretty convenient and brings you even closer to the Promises’ main objective: make asynchronous code look synchronous.

In any way, you can easily use any of these async patterns in projects with thousands of JavaScripts lines, or even tens of thousands. Both of them do the job in important business software, so the choice is not about that.

We hope this article helped you to discover asynchronous patterns in JavaScript. Right now you should have a favorite one! Be sure to test patterns shown here and choose what’s the most convenient for your use case.

   

About the Author

ByteScout Team ByteScout Team of Writers ByteScout has a team of professional writers proficient in different technical topics. We select the best writers to cover interesting and trending topics for our readers. We love developers and we hope our articles help you learn about programming and programmers.  
prev
next