DEV Community

Veronika Šimić
Veronika Šimić

Posted on • Updated on

The ultimate guide to async JavaScript

yellow corgi

On your path to becoming a JavaScript developer you will probably encounter callbacks, promises, and async/await.

JavaScript at its core is a synchronous or single-threaded language. What this means is that each action gets executed one by one and each action depends on the execution of the previous.

Think of it as cars waiting at the traffic light. Each car has to wait for the previous one to start. 🚗 🚕

const firstCar = 'first car';
const secondCar = 'second car';

console.log('Start ' + firstCar);
console.log('Start ' + secondCar);

Enter fullscreen mode Exit fullscreen mode

Output

Start first car
Start second car
Enter fullscreen mode Exit fullscreen mode

But what happens if the first car is broken? Should all the other cars wait? Who has time for that?

const firstCar = 'broken';
const secondCar = 'second car';

if (firstCar === "broken") {
  throw Error("The first car is broken. Everybody stop.");
}


console.log('Start ' + firstCar);
console.log('Start ' + secondCar);
Enter fullscreen mode Exit fullscreen mode

Output

Error: The first car is broken. Everybody stop.
Enter fullscreen mode Exit fullscreen mode

Well wouldn't it be better that each car does not depend on the previous one? Why should we care if some car is broken? If my car works why should I wait for someone to start their car? Can't I just go around it?

Well that's what asynchronous Javascript allows us to do. It creates another "lane" for us. Asynchronicity means that if JavaScript has to wait for an operation to complete, it will execute the rest of the code while waiting. We can move our actions out of the main lane and execute them at their own pace, let them do their own thing. And how do we acomplish that?

By using callbacks, promises and async/await.

Callbacks

Callbacks are functions which are nested inside another function as an argument. They can be used as part of synchronous or asynchronous code.

A synchronous callback is executed during the execution of the high-order function that uses the callback.

function startFirst(car, callback) {
  console.log("Start " + car);
  callback();
}

// callback function
function startSecond() {
  console.log("Start second car");
}

// passing function as an argument
startFirst("first car", startSecond);
Enter fullscreen mode Exit fullscreen mode

Output

Start first car
Start second car
Enter fullscreen mode Exit fullscreen mode

We can also make callbacks part of asynchronous Javascript.

An asynchronous callback is executed after the execution of the high-order function that uses the callback. If our car is broken then we will take it to mechanic and after that we can use it again. First we have to wait for some time to fix the car, which we will simulate by using setTimeout, and then we can enjoy driving our newly fixed car.

function fixMyCar(car) {
  setTimeout(() => {
    console.log(`Fixing your ${car}.`);
  }, 1000);
}

function driveMyCar(car) {
  console.log(`Driving my "new" ${car}.`);
}

let myCar = "Yellow Corgi CC85803"; // no wonder it's broken

fixMyCar(myCar);
driveMyCar(myCar);

Enter fullscreen mode Exit fullscreen mode

Output

Driving my "new" Yellow Corgi CC85803.
Fixing your Yellow Corgi CC85803.
Enter fullscreen mode Exit fullscreen mode

Javascript first executed the synchronous code (in our case call to the driveMyCar() function) and then after 1000 miliseconds it loged the result of the fixMyCarFunction().

But wait. How can we drive our car if it wasn't fixed yet?

We have to pass the driveMyCar() function as callback to the fixMyCar() function. That way driveMyCar() function isn't executed until the car is fixed.

function fixMyCar(car, callback) {
  setTimeout(() => {
    console.log(`Fixing your ${car}.`);
    callback(car);
  }, 1000);
}

function driveMyCar(car) {
  console.log(`Driving my "new" ${car}.`);
}

let myCar = "Yellow Corgi CC85803";

fixMyCar(myCar, driveMyCar);

Enter fullscreen mode Exit fullscreen mode

Output

Fixing your Yellow Corgi CC85803.
Driving my "new" Yellow Corgi CC85803.
Enter fullscreen mode Exit fullscreen mode

Well this is great we have fixed our car and now we can drive it.

But what if our car couldn't been fixed? I mean no suprise there, have you seen it? How are we going to handle errors? And what about fixing multiple cars every day?

Let's see it in action.

function fixMyCar(car, success, failure) {
  setTimeout(() => {
    car ? success(car) : failure(car);
  }, 1000);
}

const car1 = "Yellow Corgi CC85803";
const car2 = "Red Peel Trident";
const car3 = "Blue Yugo GV";

fixMyCar(
  car1,
  function (car1) {
    console.log(`Fixed your ${car1}.`);
    fixMyCar(
      car2,
      function (car2) {
        console.log(`Fixed your ${car2}.`);
        fixMyCar(
          car3,
          function (car3) {
            console.log(`Fixed your ${car3}.`);
          },
          function (car3) {
            console.log(`Your ${car3} car can not be fixed.`);
          }
        );
      },
      function (car2) {
        console.log(`Your ${car2} car can not be fixed.`);
      }
    );
  },
  function (car1) {
    console.log(`Your ${car1} car can not be fixed.`);
  }
);

Enter fullscreen mode Exit fullscreen mode

Output

Fixed your Yellow Corgi CC85803.
Fixed your Red Peel Trident.
Fixed your Blue Yugo GV.
Enter fullscreen mode Exit fullscreen mode

Is your head spinning trying to figure this out? Don't worry you aren't alone.
There's a reason why this is called callback hell or pyramid of doom. 🔥
Plus notice that if one of the cars is totaled, that is if it's unrecognizable (undefined), other cars won't even get the chance to be fixed. Now isn't that just sad? Don't we all deserve a second chance? 😊

function fixMyCar(car, success, failure) {
  setTimeout(() => {
    car ? success(car) : failure(car);
  }, 1000);
}

const car1 = "Yellow Corgi CC85803";
const car2 = undefined;
const car3 = "Blue Yugo GV";

fixMyCar(
  car1,
  function (car1) {
    console.log(`Fixing your ${car1}.`);
    fixMyCar(
      car2,
      function (car2) {
        console.log(`Fixing your ${car2}.`);
        fixMyCar(
          car3,
          function (car3) {
            console.log(`Fixing your ${car3}.`);
          },
          function (car3) {
            console.log(`Your ${car3} car can not be fixed.`);
          }
        );
      },
      function (car2) {
        console.log(`Your ${car2} car can not be fixed.`);
      }
    );
  },
  function (car1) {
    console.log(`Your ${car1} car can not be fixed.`);
  }
);

Enter fullscreen mode Exit fullscreen mode

Output

Fixing your Yellow Corgi CC85803.
Your undefined car can not be fixed.
Enter fullscreen mode Exit fullscreen mode

How can we avoid all of this? Promises to the rescue!

Promises

A Promise is an object that can be used to get the outcome of an asynchronous operation when that result isn't instantly available.

Since JavaScript code runs in a non-blocking manner, promises become essential when we have to wait for some asynchronous operation without holding back the execution of the rest of the code.

A JavaScript promise is an object that has one of the three states.

Pending - the promise still hasn't resolved (your car is at mechanic)
Fullfiled - the request was successful (car is fixed)
Rejected - the request failed (car can not be fixed)

To create a Promise in JavaScript use the new keyword and inside the constructor pass the executor function. This function is then responsible for resolving or rejecting the promise.

Let's imagine the following scenario. If our car will be fixed, then we can go on a holiday. There we can go sight seeing, then we can take some pictures, post them on social media because that's what cool kids do these days. But if our car can't be fixed then we will have to stay at our sad, little, dark apartment with our cat.

Let's write our steps.

  1. Mechanic makes a promise, she will fix our car 🔧
  2. Fixing the car means going to holiday. 🌴
  3. Once there we can go sight seeing 🌄
  4. We will take some pictures 📷
  5. After that we will post them on social media 📱

(again we are using setTimeout to simulate asynchronicity)

const mechanicsPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const fixed = true;
    if (fixed) resolve("Car is fixed.");
    else reject("Car can not be fixed.");
  }, 2000);
});

console.log(mechanicsPromise);

Enter fullscreen mode Exit fullscreen mode

Output

Promise { <pending> }
Enter fullscreen mode Exit fullscreen mode

And if you check the console you will get:

Promise { <pending> }

[[Prototype]]: Promise
[[PromiseState]]: "pending"
[[PromiseResult]]: undefined
Enter fullscreen mode Exit fullscreen mode

But wait how is PromiseResult undefined? Hasn't your mechanic told you that she will try to fix your car? No, your mechanic did not trick you. What we forgot to do is consume our Promise. And how do we do that? By using .then() and .catch() methods.

const mechanicsPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const fixed = true;
    if (fixed) resolve("Car is fixed.");
    else reject("Car can not be fixed. Go home to your cat.");
  }, 2000);
});

mechanicsPromise
  .then((message) => {
    console.log(`Success: ${message}`);
  })
  .catch((error) => {
    console.log(error);
  });

Enter fullscreen mode Exit fullscreen mode

Output

Success: Car is fixed.
Enter fullscreen mode Exit fullscreen mode

Let's check our Promise object in the console.

Output

[[Prototype]]: Promise
[[PromiseState]]: "fullfiled"
[[PromiseResult]]: Success: Car is fixed.
Enter fullscreen mode Exit fullscreen mode

As you can see from the code block above we use .then() to get the result of the resolve() method and .catch() to get the result of the reject() method.

Our car is fixed and now we can go on our holiday and do everything we planed.

.then() method returns a new Promise with a value resolved to a value, we can call the .then() method on the returned Promise like this:

const mechanicsPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const fixed = true;
    if (fixed) resolve("Car is fixed.");
    else reject("Car can not be fixed.");
  }, 2000);
});

mechanicsPromise
  .then((message) => {
    console.log(`Success: ${message}`);
    message = "Go sight seeing";
    return message;
  })
  .then((message) => {
    console.log(message);
    message = "Take some pictures";
    return message;
  })
  .then((message) => {
    console.log(message);
    message = "Posting pictures on social media";
    console.log(message);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log("Go home to your cat.");
  });

Enter fullscreen mode Exit fullscreen mode

Output

Success: Car is fixed.
Go sight seeing.
Take some pictures.
Posting pictures on social media.
Go home to your cat.
Enter fullscreen mode Exit fullscreen mode

As you can see after each call to the .then() method we chained another .then() with the message resolved to the previous .then().
We also added the .catch() to catch any errors we might have.
If we go or don't go to our holiday we will certanly have to go back home.
That's what .finally() does, this method is always executed whether the promise is fulfilled or rejected. In other words, the finally() method is executed when the promise is settled.

Our code looks a little bit nicer then when we were using callbacks. But we can make this even better with a special syntax called “async/await”. It allows us to work with promises in a more comfortable fashion.

Async/await

Async/await allows us to write promises but the code will look synchronous although it's actually asynchronous. Under the hood we are still using Promises. Async/await is syntactic sugar, which means althought it does not add any new functionality to our code, it's sweeter to use. 🍬

I don't know about you but I don't belive it until I see it.

const mechanicsPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const fixed = true;
    if (fixed) resolve("Car is fixed.");
    else reject("Car can not be fixed.");
  }, 2000);
});

async function doMyThing() {
  let message = await mechanicsPromise;
  console.log(`Success: ${message}`);
  message = "Go sight seeing";
  console.log(message);
  message = "Take some pictures";
  console.log(message);
  message = "Posting pictures on social media";
  console.log(message);
  console.log("Go home to your cat.");
}

doMyThing()
Enter fullscreen mode Exit fullscreen mode

Output

Success: Car is fixed.
Go sight seeing.
Take some pictures.
Posting pictures on social media.
Go home to your cat.
Enter fullscreen mode Exit fullscreen mode

As you can see await keyword makes the function pause the execution and wait for a resolved promise before it continues. Await keyword can only be used inside an async function.

Hey but what if my car is broken? How do I handle errors with this new syntax?

Fear not. We can use try/catch block.

const mechanicsPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const fixed = false;
    if (fixed) resolve("Car is fixed.");
    else reject("Car can not be fixed.");
  }, 2000);
});

async function doMyThing() {
  try {
    let message = await mechanicsPromise;
    console.log(`Success: ${message}`);
    message = "Go sight seeing";
    console.log(message);
    message = "Take some pictures";
    console.log(message);
    message = "Posting pictures on social media";
    console.log(message);
    console.log("Go home to your cat.");
  } catch (error) {
    console.log(error);
  }
}

doMyThing();

Enter fullscreen mode Exit fullscreen mode

Output

Your car can not be fixed. 
Enter fullscreen mode Exit fullscreen mode

Only use try/catch block if an operation is awaited. Otherwise an exception isn't going to be catched.

So even if your car is broken and now you have to ride the bus at least you learned about asynchronous JavaScript. 😄

Latest comments (18)

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍

Collapse
 
suyog28 profile image
Suyog Muley

Thank You ! Nice explanation with Car Examples.

Collapse
 
veronikasimic_56 profile image
Veronika Šimić

Thanks Suyog

Collapse
 
valx01p profile image
Pablo Valdes

You know as a new developer still learning I've been sorta confused on async/await and it's purpose for a while now. Well I just wanted to say you've made it very clear in a simple and engaging way for me and I really appreciate that, I really enjoyed reading the article, thanks 🙂

Collapse
 
veronikasimic_56 profile image
Veronika Šimić • Edited

Thank you Pablo. I am glad this blog post helped you.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
veronikasimic_56 profile image
Veronika Šimić

Great, thank you Abhay

Collapse
 
ashish_sharma profile image
Ashish

The explaination is very lucid and clear. Thanks for the great effort and dedication ☺

Collapse
 
veronikasimic_56 profile image
Veronika Šimić

Thank you Ashish, I am glad you like it

Collapse
 
muhammadiqbalid83 profile image
Muhammad Iqbal

thanks

Collapse
 
veronikasimic_56 profile image
Veronika Šimić

You are welcome Muhammad.

Collapse
 
coderdannie profile image
Emmanuel Daniel

Thanks
Your explanation was clear enough

Collapse
 
veronikasimic_56 profile image
Veronika Šimić

Thank you Emmanuel, glad it helped

Collapse
 
bhanukiranmaddela profile image
bhanukiranmaddela

Thanks for beautiful explanation

Collapse
 
hvukov profile image
Hrvoje Vukov

Excellent and funny article.

Collapse
 
veronikasimic_56 profile image
Veronika Šimić

Thanks Hrvoje I am glad it helped

Collapse
 
brunoperkovic profile image
BrunoPerkovic

Great article, really helped me understand async via examples

Collapse
 
veronikasimic_56 profile image
Veronika Šimić

Thanks Bruno, glad to hear it.