Why do we even need asynchronous calls?

Asynchronous calls may cause a lot of confusion, but they are needed. Why, you may ask. This is because JavaScript is single-threaded and because of that, when the event loop is processing a JS code, it can't process DOM updates – that means no repainting, no interactions and so on. You can play with a small example here: http://event-loop-tests.glitch.me/while-true-test.html Press the button and try to highlight the text (on Firefox and Safari it even freezes the GIF!)

Imagine a situation when your script connects to an API to retrieve some data from it and the whole page freezes and becomes noninteractive. A nightmare from the user's perspective! Especially nowadays, when JS is not only used as a language for simple interactions, but is responsible for building whole Apps and integrating with multiple APIs.

That is the reason why understanding asynchronous JS is so important, especially for developers who want to create backends based on JS, where the effective usage of the event loop is crucial for performance.

Callbacks

At the beginning there were callbacks… I mean chaos! Sorry, I often mix up these two concepts.

What is a Callback?

A callback is a very simple concept, it is a function that is passed as an argument to another function to be called by it. I'm certain that you use it on a daily basis, even if unconsciously. It happens whenever you attach an event listener with EventTarget.addEventListener. addEventListener is an EventTarget's method that takes at least two arguments - type and listener, which is a function that we want to be executed when the specified event occurs.

Why the jokes?

I've proven that callbacks are useful and widely used, so one may ask why there are so many jokes about them? There is no problem with callbacks themselves, but how they are used.

The contract of an asynchronous function built with callbacks looks like this: asyncFunction(params, callback).

As developers, we usually want to perform one action after another, often asynchronously, so it is possible to end up with a code that looks like that:

doAsyncCall(params, function(error, results) {
  if (error) {
    handleErrors(error);
  } else {
    doSecondAsyncCall(results, function(error, resultsFromSecondCall) {
      if (error) {
        handleErrors(error);
      } else {
        console.log('results from the second async call', resultsFromSecondCall);
      }
    });
  }
});

Imagine adding a few more branches and making some asynchronous calls within error handlers – this code will quickly become unmaintainable and extremely unreadable.

This is called... Chaos hell, I mean Callback hell!

We may, of course, extract each of these inlined functions to a separate function expression, but these functions are often so specific to that one use case, that we end up with a bunch of functions that are hard to reuse.

We could also ignore all of these error checks, but… thinking only about a happy path is considered a bad practice.

Callback Hell

If you are interested in reading more about callback hell and how to avoid it, here's a useful link. Callback Hell

This is how callback hell may look like:

a(resultsFromA => {
  b(resultsFromA, resultsFromB => {
    c(resultsFromB, resultsFromC => {
      d(resultsFromC, resultsFromD => {
        e(resultsFromD, resultsFromE => {
          f(resultsFromE, resultsFromF => {
            console.log(resultsFromF);
          });
        });
      });
    });
  });
});

But then… someone made

Promises

What is a promise?

A promise is a special type of object that helps us, developers, write cleaner asynchronous code. Actually, promises are the base of current asynchronous JS.

“How about async/await?” “Shh, we'll get there and then you'll see”.

The promise is just like a real life promise - something will eventually happen.

Imagine you are in a pizzeria ordering a pizza (just like JS when it orders some resources from the web). The cashier (browser) makes us a promise that once the pizza is ready, he will bring it to you and THEN you can eat (process) it. Having this promise, you are not blocked by the cook and you don't have to wait for it near the counter, but you may sit at your table and do something more important, like scrolling Facebook.

How does it work?

As I said, promise is special type of object and it can have one of 3 states:

  • pending – promise is being processed. (e.g. fetch request is not yet resolved).
  • fulfilled – operation completed successfully (e.g. fetch request got the data)
  • rejected – something went wrong, operation failed (e.g. fetch request encountered an error)

How to construct a promise?

Promise API comes with a constructor function that takes a function as an argument. That callback also takes two functions as arguments.

const myPromise = new Promise((resolve, reject) => {})
  • resolve - invoke this function when promise should be resolved (completed). Argument passed to it will be a data returned from the promise.
  • reject - invoke this function when promise should be rejected. Argument passed to this function will be an error that can be handled.

A simple promise may look like this:

const myPromise = new Promise(resolve => setTimeout(() => resolve(10), 5000));

The following example assigns a new promise to a variable myPromise. At the beginning, this promise will be pending and after around 5s it will change its status to fullfiled

Screenshot that presents result of a previous code snippet

Note. Please ignore this 23. Console returns value from the last line. In that case - 23, which is the id of that setTimeout method.

Seeing that example in “real life” there is only one question:

How to get the value returned from promise?

I haven't said one thing about a promise object yet, but I left a small hint in the example with Pizzeria. Promises are then'able. That means we can call on them a special method from their prototype - then.

Then takes two functions (this is important and often leads to side effects) as arguments - onFulfilled (required) and onRejected (optional).

  • onFulfilled - is invoked when the promise reaches fulfilled state.
  • onRejected - is invoked when one of a previous promises reaches rejected state. Keep in mind, this function won't be executed if the error is thrown in the onFulfilled handler of the same then statement.

Then always returns a promise! Whatever is returned from one of the handlers (onFulfilled or onRejected) is wrapped in a promise. That means:

  • if a handler returns a number “then” will automatically transform this number to a fulfilled promise with a value of this number.
  • If a handler returns a promise, then this is also a returned value from then.
  • If a handler returns undefined, then returns a fulfilled promise with a value set to undefined. All functions in JS without explicit return return undefined.
  • If a handler throws an error then then returns rejected promise with value set to this error.

Because then returns another promise we can add another then statement to that promise and so on, this is called Promise chaining

If it looks complicated, don't worry, it is. However, I'll try to explain it in more details in the examples.

// this is a neat trick, if you don't need to use whole `Promise` constructor,
// but only want to transfer some data into a promise, here's the way.
const myPromise = Promise.resolve(10);

const newPromise = myPromise
  .then(data => 2 * data) // data will be 10 and we return 2*10
  // so we expect next promise to get value of 20
  .then(console.log) // we pass `console.log` function as an argument, which will print `20`.
  // keep in mind that previous `then` got `console.log` as an argument.
  .then(data => console.log(data)) //Console.log returns undefined, so this one will print `undefined`
  .then(
    () => {
      throw new Error('Example error');
    },
    error => console.log(error)
  ) // error thrown in `onFulfilled` handler is not captured by `onRejected` in the same `then`.
  .then(() => console.log(`I'll do nothing, because there is an error`))
  .then(
    () => {},
    () => {
      console.log(`However, I'll catch this error and I will handle it by returning 40`);
      return 40;
    }
  )
  .then(data => console.log(`Hey! I've got ${data}`))
  .then(() => {
    throw new Error(`catch example`);
  }) // how can I catch this error, do I need to create a dummy `then` statement?
  .catch(error => console.log(`No! I'm a Catch statement and I will handle this error: ${error}`))
  // catch is similar to `then(_, onReject)`
  .then(() => console.log(`I can attach to the catch, because catch also returns a promise!`));

Previous example with the results in the console

Keep in mind that errors travel down the promise chain and they're handled by:

  • the first then statement with onRejected handler
  • the first catch statement.

Whatever comes first!

And a small diagram from MDN that illustrates how promise chaining works.

Diagram that presents how promise chaining works

It is considered a good practice to finish the promise chain with a catch statement!

Promises vs Callbacks

Are promises better than callbacks? In a way, but there is no easy answer to that question. Promises use callbacks, we've seen that, we've written code - then takes 2 callbacks, Promise constructor takes 2 callbacks. Sometimes we don't have a choice, fetch returns a promise, XHR uses events, so indirectly callbacks. Of course, we may write a promise-based code in a callback or write a callback-based code inside a promise. However, I would say that's not a good practice, mixing these 2 paradigms in one code won't end well because it will only lead to more confusion inside.

Usually, callbacks shift our code right, create lots of branches and deep nesting (callback hell aka pyramid of doom) but there are ways to reduce it!

Promises, on the other hand, help us to write more linear code thanks to the chaining mechanism. However, it's possible to write promise hell, but it's usually a result of a lack of familiarity with promises.

getUsersIDs().then(userIDs => {
  return getUserName(userIDs[0]).then(userData => {
    return getUsersByName(userData.name).then(users => console.log(`There are ${users.length} users with that name`));
  });
});

Here's the way how to fix example with promise hell.

getUsersIDs()
  .then(userIDs => getUserData(userIDs[0]))
  .then(userData => getUsersByName(userData.name))
  .then(users => console.log(`There are ${users.length} users with that name`));

There is one more downside of callbacks - error handling. When using callbacks, one must add another callback to handle it or use tons of if statements in the code to check whether there was an error. Therefore, it's hard to follow it. This issue is a bit simpler when writing promise-based code. Promise API gives us two functions: then with its onRejected parameter, and catch. With them, reasoning about error handling is much easier!

Promises have 2 big advantages. We may write a function that returns a promise to which we may attach a then handler, we may also attach multiple thens to one promise.

function getPromise() {
  return Promise.resolve(20);
}

const promise = getPromise();

promise.then(data => console.log(data)); // 20;
promise.then(data => console.log(2 * data)); // 40;

Both thens will be executed as soon as promise fulfills.

If I were to choose between callbacks and promises I would suggest writing a promise-based code. I find it easier to follow the code flow, usually each then is one under another and with that we avoid problems with too deep nesting.

Conclusion

I hope that now promises hold no more mysteries for you. However, If you are interested in further reading, I strongly recommend reading We have a problem with promises by pouchDB. This article helped me a lot in understanding this complex topic.

As always, when you find yourself in a situation when a method doesn't work as you expect, don't be afraid to look into the documentation or MDN.

The next stop in our journey today is…

Generators

  • Wait... what? What do the generators have in common with async functions?

Because generators have a special ability to pause their execution and that is what we want to do during handling async functions.

First things first:

What are the generators?

Generators are a new type of JS function introduced in ES6. They have a special ability, but more about it in a minute.

How do we create a generator function? It's easy.

function* myAwesomeGeneratorFunction() {
  /* ... */
}

and that's all. This small asterisk and our function happens to be a generator.

Ok, but why would we want to create generators? As I said at the beginning, it's because they have a superpower - they can… pause their execution. Dam dam dam! Isn't that great? We can pause a function and get back to it after a while and resume its execution.

How does it work?

function* myAwesomeGeneratorFunction() {
  console.log('Function in progress');

  yield; // yield is a special statement that pauses the execution.
  console.log('function resumes');
}

const iterator = myAwesomeGeneratorFunction();

/* keep in mind that nothing is printed here. It's because when we call our generator, it firstly
returns an iterator. We may say that it starts in a paused mode. To resume execution we must call 
`next` method on the iterator. */

let result = iterator.next();

/* now you can see “Function in progress” printed to the console, but no "function resumes". 
Just like we expected.
Let's see what's inside of this `result`  */

console.log(result); // {value: undefined, done: false}

result = iterator.next(); // now you can see “function resumes” printed to the console.
console.log(result); // {value: undefined, done: true}

/* that `done: true` implies that the function has finished its execution, 
but what happens if we call `next` one more time? */
result = iterator.next();
console.log(result); // {value: undefined, done: true}, nothing happened - the value is the same.

Meme with Morpheus saying "What if I told you, you may return multiple values from generator?" via Imgflip GIF Maker

Will the following example work?

function* myAwesomeGeneratorFunction() {
  return 20;
  return 30;
  return 40;
}

const iterator = myAwesomeGeneratorFunction();

iterator.next(); // {value: 20, done: true}

// done true? Let's check what would happened if we call `next` one more time
iterator.next(); // {value: undefined, done: true}
iterator.next(); // {value: undefined, done: true}

No more data, the function execution has finished after the first return. Have I lied to you? No. Unfortunately, it doesn't work like that. How can we achieve that? We need to use our special keyword - yield. Let's see that on the following example.

function* myAwesomeGeneratorFunction() {
  yield 20;
  yield 30;
  yield 40;
}

const iterator = myAwesomeGeneratorFunction();
iterator.next(); // {value: 20, done: false}
iterator.next(); // {value: 30, done: false}
iterator.next(); // {value: 40, done: false}
iterator.next(); // {value: undefined, done: true}

There is a small difference between finishing function using yield and return.

function* myAwesomeGeneratorFunction() {
  yield 20;
  yield 30;
  return 40;
}

const iterator = myAwesomeGeneratorFunction();
iterator.next(); // {value: 20, done: false}
iterator.next(); // {value: 30, done: false}
iterator.next(); // {value: 40, done: true}
iterator.next(); // {value: undefined, done: true}

The only difference lies with the value of done: true. return returns a value and sets done to true in opposite to yield. The interesting question is what does it imply?

function* myGenerator() {
  yield 20;
  yield 30;
  return 40;
}

const iterator = myGenerator();

// we can spread iterators
console.log(...iterator); // 20 30

Where's our 40? It's been "eaten". We may say that the spread operator "calls" next method and returns values until done is true. Which happens, as we already know, when we call return.

Let's check out what happens if we use yield instead of return.

function* myGenerator() {
  yield 20;
  yield 30;
  yield 40;
}

const iterator = myGenerator();

// we can spread iterators
console.log(...iterator); // 20 30 40

Now everything works!

Returning value back to the generator

Yield has another super power, with it, it's possible to return value back to the function:

function* generateAddFunction() {
  const first = yield;
  const second = yield;

  console.log(first + second);
}

const iterator = generateAddFunction();
iterator.next(); // we start execution of our paused function
iterator.next(42); // we pass 42 back to the generator
iterator.next(23); // we pass 23 back to the generator and 65 is printed to the console.

Combining knowledge

Now you know that generators can pause execution of the function and return value back to the function. Can you image combining generators and promises? Let me show you something:

function* processDummyToDo(id) {
  const dummyUrl = `https://jsonplaceholder.typicode.com/todos/${id}`;
  const data = yield fetch(dummyUrl); // let's wait for the data from the fetch.
  console.log(data); // {userId: 1, id: 1, title: "delectus aut autem", completed: false}
}

const iterator = processDummyToDo(1);
iterator
  .next()
  .value // value contains a promise returned by `fetch`
  .then(response => response.json())
  .then(data => iterator.next(data)); // now we pass back the data from the request to the function.

Does it look familiar now? Yes! It looks almost like async/await, which is our last stop for today.

Small note, these are basics of the generators, there is much more into them and I encourage you to learn more about them! For example here: https://codeburst.io/what-are-javascript-generators-and-how-to-use-them-c6f2713fd12e.

Async/Await

This is a well-known syntax introduced in ES7. Its main benefit is that we can pause the execution of the function and wait for the promise to resolve. Almost the same as what we saw in the last example involving generators. The biggest advantage of this, compared to promises, is that it comes with a bit of syntactic sugar. In case of promises, when we want to do something with the data from async function, we need to chain another promise because there is no other way to tell the engine to wait for the results.

Let's dive into this feature.

Await

A special keyword that pauses function execution until the promise it awaits resolves. Have you noticed the magic word I used? Promise. Async/Await works with promises and only with promises. You can only await the resolution of promises. That's what I mean by saying that promises are the base of current asynchronous JS.

//...
// we wait for the promises returned from `fetch` to resolve.
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
let data = await response.json(); // we wait for the response to be resolved into the JS object.
console.log(data); // {userId: 1, id: 1, title: "delectus aut autem", completed: false}

// we can of course wait for the chain of promises - the promise chain returns a promise after all.
data = await fetch('https://jsonplaceholder.typicode.com/todos/1').then(res => res.json());
console.log(data); // {userId: 1, id: 1, title: "delectus aut autem", completed: false}
//...

This is not the final example! It won't work yet. I believe you know why, but let me surprise you.

I find this solution convenient and much more elegant than the example with generators.

Remember - await works only with promises!

Async

Another special keyword is needed - otherwise we wouldn't call this syntax async/await. If you want to use await keyword, you need to wrap this function in async. It's the law. This is because you need to somehow tell the JS engine that this function is asynchronous and you may use await inside.

So the working example from the previous section would look like:

async function asyncFunction() {
  data = await fetch('https://jsonplaceholder.typicode.com/todos/1').then(res => res.json());
  console.log(data); // {userId: 1, id: 1, title: "delectus aut autem", completed: false}
}

asyncFunction();

Let's consider a little more complex example

async function getDummyToDo(id) {
  const data = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then(response => response.json());
  console.log('getDummyToDo', data);
  return data;
}

function processDummyToDo(id) {
  const toDo = getDummyToDo(id);
  console.log('processDummyToDo', toDo);
}

processDummyToDo(1);

Do you have an idea what's wrong with this example? Let's go step by step!

  1. We call processDummyToDo function with 1 as an argument.
  2. processDummyToDo will call getDummyToDo with the same argument.
  3. getDummyToDo calls fetch and waits for it to resolve.
  4. Because the initiator is not an async function, JS can't pause its execution. That's why getDummyToDo returns a promise.
  5. processDummyToDo assigns that promise to a variable toDo
  6. toDo variable is printed to the console - processDummyToDo Promise {<pending>}
  7. fetch promise resolves
  8. processDummyToDo prints - getDummyToDo {userId: 1, id: 1, title: "delectus aut autem", completed: false} on the console.
  9. getDummyToDo returns a toDo.
  10. Promise initially returned by getDummyToDo resolves with the value returned from it.

Working example would look as follows

async function getDummyToDo(id) {
  const data = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then(response => response.json());
  console.log('getDummyToDo', data);
  return data;
}

async function processDummyToDo(id) {
  const toDo = await getDummyToDo(id);
  console.log('processDummyToDo', toDo);
}

processDummyToDo(1);

Or, using the promise approach:

async function getDummyToDo(id) {
  const data = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`).then(response => response.json());
  console.log('getDummyToDo', data);
  return data;
}

function processDummyToDo(id) {
  getDummyToDo(id).then(toDo => console.log('processDummyToDo', toDo));
}

processDummyToDo(1);

Lessons learned:

  1. async makes all values returned from it wrapped in a promise.
  2. Still, asynchronicity is like a disease, it makes each function it touches asynchronous, at least if there is a need to use a value returned from it.

What about errors?

Async/await gives us one more superpower, it allows us to use try/catch again! What does it mean? If the promise we await is rejected, it throws an error that we can catch.

async function asyncFunction() {
  try {
    await Promise.reject('This promise throws');
  } catch (e) {
    console.log('The promise threw and I caught it - ', e);
  }

  console.log('I work normally!');
}

asyncFunction();

Running our example will result in the following lines being printed to the console:

  1. The promise threw and I caught it - This promise throws
  2. I work normally!

There is no need for a Promise.catch statement or whatever. What's more, there is no error in the console!

Combining with Promises

But what if you want to create a few requests simultaneously?

function constructTimeoutPromise(time, name) {
  return new Promise(resolve => {
    setTimeout(() => resolve(name), time);
  });
}

async function synchronousRequests() {
  const start = performance.now(); //start the performance measuring

  const name1 = await constructTimeoutPromise(3000, 'foo');
  const name2 = await constructTimeoutPromise(2000, 'bar');
  const name3 = await constructTimeoutPromise(4000, 'baz');

  console.log('it took only', performance.now() - start, 'ms to perform the requests');
}

synchronousRequests();

At the end of the function, we may see that our function took more than 9s to finish the function, but we want to call them at once, because these requests are independent of one another. How to achieve that? Promises to the rescue!

function constructTimeoutPromise(time, name) {
  return new Promise(resolve => {
    setTimeout(() => resolve(name), time);
  });
}

async function asynchronousRequests() {
  const start = performance.now(); //start the performance measuring

  const promise1 = constructTimeoutPromise(3000, 'foo');
  const promise2 = constructTimeoutPromise(2000, 'bar');
  const promise3 = constructTimeoutPromise(4000, 'baz');

  const data = await Promise.all([promise1, promise2, promise3]);

  console.log('it took only', performance.now() - start, 'ms to perform the requests');
  console.log(data);
}

asynchronousRequests();

And now it works as we wanted! The previous example will result with printing:

  1. it took only 4002.859999993816 ms to perform the requests
  2. (3)["foo", "bar", "baz"] - As you remember Promise.all returns a promise that resolves with an array of all values "returned" from promises.

Summary

In this article I described a brief history of asynchronicity in JS. We have seen why asynchronous operations are important in the JS world. We came to understand what callbacks are and got to know generators, which might be most interesting to you. Most importantly, we learned what async/await does and how it works under the hood together with promises, which I hope don't have any mysterious for you anymore.

If you have any questions, look for the answers on MDN or reach out to me. I'll do my best to answer all your questions.