async and await
Imagine your JavaScript code is a kitchen, and you have multiple tasks cooking at once.
Promises made asynchronous programming more manageable, letting you “start the pasta”
and then “marinate the chicken” without blocking. Yet, chaining .then can
still get bulky.
Enter async and await. In 2017, JavaScript
introduced a way to write asynchronous code that looks synchronous. In essence,
you can pause a function until a promise returns, making your code more
intuitive, compact, and readable.
By the end of this lesson, you’ll be able to:
async keyword on a function to return an implicit Promiseawait inside an async function to handle promise results synchronously
Let’s review how we normally handle promises. Suppose you have
walkTheDog() returning a promise that resolves to "happy dog" after one second.
Then, doChores() is your “main” function:
function walkTheDog() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('happy dog');
}, 1000);
});
}
function doChores() {
console.log('before walking the dog');
walkTheDog()
.then(res => {
console.log(res);
console.log('after walking the dog');
});
return 'done';
}
console.log(doChores());
// prints:
// before walking the dog
// done
// happy dog
// after walking the dog
Notice that doChores() logs “done” before the dog walk promise
resolves. That’s because the promise is asynchronous. If you need to do something
after the dog walk completes, you chain .then. This works, but chaining multiple
promises can become bulky, especially for more complex tasks.
async Function Declarations
Marking a function with async automatically wraps its return value in a
promise. For example:
async function doChores() {
return 'done';
}
console.log(doChores());
// prints:
// Promise { 'done' }
So, doChores() doesn’t just return “done”—it returns a promise that’s already
resolved with the value “done.” This alone isn’t too exciting, but it allows us to use
await inside that function.
awaiting a Promise
The await keyword pauses execution within an async function
until the promise fulfills (or rejects). You can’t use await at the top level
of your code—only inside an async function. Let’s rewrite the “walk the dog” scenario using
async and await:
function walkTheDog() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('happy dog');
}, 1000);
});
}
async function doChores() {
console.log('before walking the dog');
const res = await walkTheDog();
console.log(res);
console.log('after walking the dog');
}
doChores();
// prints:
// before walking the dog
// happy dog
// after walking the dog
Observe how much more natural it reads: we “pause” on
await walkTheDog() until the promise fulfills, then log “happy dog.”
Meanwhile, JavaScript still behind the scenes handles everything asynchronously.
Remember that doChores() itself returns an implicit promise. Once all its
statements finish, that promise resolves. You can handle it with .then:
async function doChores() {
console.log('before walking the dog');
const res = await walkTheDog();
console.log('after walking the dog');
return res.toUpperCase();
}
doChores().then(result => console.log(result));
// prints:
// before walking the dog
// after walking the dog
// HAPPY DOG
The then call sees res.toUpperCase() as the fulfilled
value of doChores(). We can’t just await doChores() at the top level
in older versions of JavaScript runtime—only inside another async function.
async Wrapper
If you want to await doChores(), you can do so within another
async function, like this:
function walkTheDog() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('2');
resolve('happy dog');
}, 1000);
});
}
async function doChores() {
console.log('1');
const res = await walkTheDog();
console.log('3');
return res.toUpperCase();
}
async function wrapper() {
console.log('0');
const finalResult = await doChores();
console.log('4');
console.log(finalResult + '!!!');
}
wrapper();
// prints:
// 0
// 1
// 2
// 3
// 4
// HAPPY DOG!!!
Execution order:
wrapper().doChores() starts.walkTheDog() resolves (“2”).await walkTheDog() is done.wrapper(), after await doChores(), we log “4” and then the result.Suppose you have a typical promise chain that prints resolved values in sequence:
function wrapper() {
promise1
.then(res1 => {
console.log(res1);
return promise2;
})
.then(res2 => {
console.log(res2);
return promise3;
})
.then(res3 => {
console.log(res3);
});
}
Refactoring to async/await is straightforward:
async function wrapper() {
console.log(await promise1);
console.log(await promise2);
console.log(await promise3);
}
The difference in readability is striking. It’s simpler to follow the sequence top to bottom without nesting.
Since async/await appears more synchronous, you can handle errors with a normal
try...catch:
function action() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('uh-oh'); // rejected
}, 3000);
});
}
async function handlePromise() {
try {
const res = await action();
console.log('resolved with', res);
} catch (err) {
console.log('rejected because of', err);
}
}
handlePromise();
// prints:
// rejected because of uh-oh
No more separate .then() + .catch()—it feels like a normal
synchronous try/catch but is still asynchronous under the hood.
In this lesson, you discovered how JavaScript’s async and await
keywords enable you to write code that looks synchronous yet remains asynchronous.
In essence:
async, and JavaScript automatically wraps its return
value in a promise.
await inside an async function to pause execution until a promise
fulfills (or rejects).
try...catch, just like you would do
synchronously.
The result is clean, linear code that’s easier to maintain. By leveraging async/await, you’ll transform your promise-heavy logic into something that reads like a normal sequence of steps—without sacrificing concurrency or the non-blocking nature of JavaScript.