Breakdown of Async/Await in Javascript

Breakdown of Async/Await in Javascript

Callbacks, Promises, Iterators & Generators

ยท

5 min read

Nowadays, when we are making an API request, we are writing something like

const res = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${location}&appid=${key}`);
console.log(res);

But how does it work under the hood, and how did it come here?

Callbacks

Before await/async, there were callbacks. Pretty basic idea we are passing a function that we want to be executed when response arrives

function handleResponse(err, res, body) {
  // Do stuff
}
request('https://www.somepage.com', handleResponse);

However, this quickly becomes hard to handle because, in the real world, you have multiple asynchronous tasks that depend on each other, so that you will end up with a lot of nested callbacks known as callback hell.

Promise

To make asynchronous work easier Promise were introduced, promises are objects that holds three properties: an "array" of methods that need to be executed when response arrives, to add functions to this "array" we can use then, catch, and/or finally, state of promise fulfilled, rejected or pending, and result/response. image.png We can manually create a promise object like so

const examplePromise = new Promise((resolve, reject) => {
// do stuff that takes time
// then resolve(/*response*/); // triggers functions added passed with then
// or reject(/*error response*/); // triggers functions added passed with catch
});

or we can call method that returns promise

const weatherPromise = fetch(`https://api.openweathermap.org/data/2.5/weather?q=${location}&appid=${key}`)
.then(res => console.log(res))
.catch(err => console.error(err))
.finally(() => console.log('Done!'));

Immediately when this line gets executed, weathePromise gets a promise object with an "array" of methods to execute after resolving the promise, with PromiseState pending and with PromiseResult undefined. And After Xms response comes, PromiseResult gets assigned value of the response, PromiseState becomes fulfilled, and necessary methods get executed. This pattern is better than callbacks and is easy to understand; however, it can also becomes ugly and hard to read.

Iterators

Iterators are functions that return new values every time you invoke them. To create an Iterator, you need to implement an iterator protocol, a protocol is basically the same as an interface in other languages. To implement it, you need an object with a method next that returns an object with two properties, value and done. You can implement it using a function that creates an iterator object, or directly adding method to an object.

function makeIterator(array) {
  let index = 0;
  return {
    next: function() {
      const result ={
          value: array[index],
          done: index >= array.length
      };
      index++;
      return result;
    }
  };
}
const iterator = makeIterator([1, 2, 3]);
console.log(iterator.next());
// { value: 1, done: false }
console.log(iterator.next());
// { value: 2, done: false }
console.log(iterator.next());
// { value: 3, done: false }
console.log(iterator.next());
// { value: undefined, done: true }

Also by adding [Symbol.iterator] property to your object now you can use your object inside of for ... of loops. (spread operator also uses that method)

const iterator = {
  index: 0,
  numbers: [1, 2, 3],
  next: function() {
    const result ={
      value: this.numbers[this.index],
      done: this.index >= this.numbers.length
    };
    this.index++;
    return result;
  },
  [Symbol.iterator]: function() { return this; }
};

for (let number of iterator) {
  console.log(number);
}
// 1
// 2
// 3
console.log([...iterator]);
// [ 1, 2, 3 ]

Instead of return this; from [Symbol.iterator] you can return and object with next() property on it, and remove next() from you object. (see example below)
Moreover, there also exists [Symbol.asyncIterator] property, defining which give your object the ability to be used in for await ... of loop ๐Ÿคฏ
For example asyncIterator can be used in case if you have a list of APIs that you need to call.

const apiList = {
  numbers: [1, 2, 3],
  [Symbol.asyncIterator]: function() {
    let index = 0;
    return {
      next: () => {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            let currentIndex = index;
            index++;
            resolve(
              {
                value: this.numbers[currentIndex],
                done: currentIndex >= this.numbers.length
              }
            );
          }, 300);
        });
      }
    };
  }
};

async function callApis() {
  for await (let number of apiList) {
    console.log(number);
  }
}
callApis();
// 1 (with 300ms delay)
// 2 (with 300ms delay)
// 3 (with 300ms delay)

Generators

Generators are kind of evolved iterators or a more convenient way of writing iterator.

function* makeIterator() {
  yield 1;
  yield 2;
  yield 3;
}
const iterator = makeIterator();
console.log(iterator.next());
// { value: 1, done: false }
console.log(iterator.next());
// { value: 2, done: false }
console.log(iterator.next());
// { value: 3, done: false }
console.log(iterator.next());
// { value: undefined, done: true }

All example from Iterator section can be rewritten using Generators

const iterable = {
  *[Symbol.iterator]() {
      yield 1;
      yield 2;
      yield 3;
  }
}
for (let value of iterable) {
  console.log(value);
}
// 1
// 2
// 3
console.log([...iterable]);
// [ 1, 2, 3 ]

The main element of Generator is the yield keyword; it returns a value and immediately freezes the execution of the function. What execution of this snipped will result in?

function* makeIterator() {
  let num = yield 1;
  yield num;
}
const iterable = makeIterator();
for (let value of iterable) {
  console.log(value);
}

Correct answer

1
undefined

We are getting yielded out before num gets assigned any value, so it becomes undefined. You can pass a value back inside of Generator by providing it as an argument to next method, and this value will be placed in a place where the last yield was executed.

function* makeIterator() {
  let num = yield 1;
  yield num;
}
const iterable = makeIterator();
console.log(iterable.next());
console.log(iterable.next(42));
// { value: 1, done: false }
// { value: 42, done: false }

Async and Await

Now, why did we go on this Iterator and Generator tangent?
Because under the hood, async functions are asynchronous generators.

function continueExecution(value) {
  caller.next(value);
}
function* createApiCaller() {
  const response = yield fetch('API_URL');
  console.log(response);
}
const caller = createApiCaller();
const call = caller.next();
call.value.then(continueExecution);
// response gets console.loged aftex Xms

Using Generator and promise, we made function createApiCaller behave the same way as if it was asynchronous async createApiCaller() ๐Ÿง™๐Ÿปโ€โ™‚๏ธ. But thanks to async/await, we can skip all this and focus on the important parts and make code way more readable.

async function callApi() {
  const response = await fetch('https://reqres.in/api/users/2');
  console.log(response);
}
callApi();
ย