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);
Output
Start first car
Start second car
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);
Output
Error: The first car is broken. Everybody stop.
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);
Output
Start first car
Start second car
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);
Output
Driving my "new" Yellow Corgi CC85803.
Fixing your Yellow Corgi CC85803.
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);
Output
Fixing your Yellow Corgi CC85803.
Driving my "new" Yellow Corgi CC85803.
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.`);
}
);
Output
Fixed your Yellow Corgi CC85803.
Fixed your Red Peel Trident.
Fixed your Blue Yugo GV.
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.`);
}
);
Output
Fixing your Yellow Corgi CC85803.
Your undefined car can not be fixed.
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.
- Mechanic makes a promise, she will fix our car 🔧
- Fixing the car means going to holiday. 🌴
- Once there we can go sight seeing 🌄
- We will take some pictures 📷
- 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);
Output
Promise { <pending> }
And if you check the console you will get:
Promise { <pending> }
[[Prototype]]: Promise
[[PromiseState]]: "pending"
[[PromiseResult]]: undefined
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);
});
Output
Success: Car is fixed.
Let's check our Promise object in the console.
Output
[[Prototype]]: Promise
[[PromiseState]]: "fullfiled"
[[PromiseResult]]: Success: Car is fixed.
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.");
});
Output
Success: Car is fixed.
Go sight seeing.
Take some pictures.
Posting pictures on social media.
Go home to your cat.
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()
Output
Success: Car is fixed.
Go sight seeing.
Take some pictures.
Posting pictures on social media.
Go home to your cat.
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();
Output
Your car can not be fixed.
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. 😄
Top comments (18)
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 🙂
Thank you Pablo. I am glad this blog post helped you.
The explaination is very lucid and clear. Thanks for the great effort and dedication ☺
Thank you Ashish, I am glad you like it
Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍
Thank You ! Nice explanation with Car Examples.
Thanks Suyog
Great, thank you Abhay
thanks
You are welcome Muhammad.
Excellent and funny article.
Thanks Hrvoje I am glad it helped
Thanks for beautiful explanation
Great article, really helped me understand async via examples
Thanks Bruno, glad to hear it.
Thanks
Your explanation was clear enough
Thank you Emmanuel, glad it helped