With older JavaScript specifications, we had to use callbacks to handle asynchronous operations. However, this led to callback hell when multiple async operations depended on each other. Callback hell makes our code messy and hard to maintain.
function wait(ms, cb) {
setTimeout(cb, ms);
}
function main() {
console.log('almost there...');
wait(2007, () => {
console.log('hold on...');
wait(2012, () => {
console.log('just a bit more...');
wait(2016, () => {
console.log('done!');
});
});
});
}
So with ES6 (ES 2015), Promise was introduced by default to solve callback hell. With Promises, our code looks closer to synchronous style, making it easier to follow and maintain. However, using Promise gave rise to a “somewhat” similar problem: Promise hell.
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
function main() {
console.log('almost there...');
wait(2007)
.then(() => {
console.log('hold on...');
return wait(2007);
})
.then(() => {
console.log('just a bit more...');
return wait(2012);
})
.then(() => {
console.log('just a bit more...');
return wait(2016);
})
.then(() => {
console.log('done!');
});
}
To solve this, ES7 (ES 2017) introduced async functions (async / await). Async functions let us write asynchronous operations in a synchronous style, making code cleaner, more readable, and “easier to understand.”
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function main() {
console.log('almost there...');
await wait(2007);
console.log('hold on...');
await wait(2012);
console.log('just a bit more...');
await wait(2016);
console.log('done!');
}
How to use
To use an async function, declare the async keyword right before the function definition keyword. For functions defined with function, declare it before function; for arrow functions, before the parameter list; for class methods, right before the method name.
// regular function
async function functionName() {
let ret = await new Google().search('JavaScript');
}
// arrow function
let arr = ['JS', 'node.js'].map(async (val) => {
return await new Google().search(val);
});
// Class
class Google {
constructor() {
this.apiKey = '...';
}
async search(keyword) {
return await this.searchApi(keyword);
}
}
With the async keyword, we can await Promise (async operations) within the function without blocking the main thread using await.
An async function always returns a Promise, whether or not you use await. This Promise will be in a fulfilled state with the result returned via return, or in a rejected state with the result thrown via throw.
Thus, the essence of an async function is Promise. If you haven’t learned about Promise yet, read up here.
With Promises, we can handle exceptions with catch quite simply. But it’s not always easy to track and read. With async functions, this is extremely simple using try catch, just like synchronous operations.
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function runner() {
console.log('almost there...');
await wait(2007);
console.log('hold on...');
await wait(2012);
console.log('just a bit more...');
await wait(2016);
throw new Error(2016);
}
async function main() {
try {
await runner();
console.log('done!');
} catch (e) {
console.log(`problem at ${e}`);
}
}
Nice! Clearly, code using async/await looks simpler, easier to follow, “easier to understand,” and solves the callback/promise hell problem. However, using it isn’t always straightforward. Let’s look at some common cases below.
Gotchas
Forgetting the async keyword
Obviously, without this keyword, you don’t have an async function and can’t use await. You might think it’s impossible to forget, but it can happen — for instance, when declaring a function inside an async function. Nested functions must also be declared with async if you want to use await inside them.
async function main() {
await wait(1000)
let arr = [100, 300, 500].map(val => wait(val))
arr.forEach(func => await func)
// ??? error
}
Confusion with the await keyword
Two typical scenarios:
-
Forgetting
awaitwhen you need to wait for an async operationIs this dangerous? Yes! If you omit
await, you’ll get aPromiseinstead of the actual result of the async operation.async function now() { return Date.now(); } async function main() { let t = now(); console.log(t); // ??? `t` is a `Promise` instance } -
Adding “unnecessary”
awaitbefore a synchronous operationAfraid of forgetting, so you sprinkle
awaiteverywhere? It’s not harmful except for two issues: you can no longer tell what’s sync and what’s async, and performance degrades. Everyawaitimplicitly expects aPromise— if it’s not a Promise, it gets wrapped in one viaPromise.resolve(value). That’s taking the long way around for1 + 0 = 1.async function main() { console.log('run with await'); let i = 1000000; console.time('await'); while (i-- > 0) { let t = await (1 + 0); } console.timeEnd('await'); console.log('run without await'); i = 1000000; console.time('normal'); while (i-- > 0) { let t = 1 + 0; } console.timeEnd('normal'); }
Forgetting error handling
Just like forgetting catch with Promises, forgetting try catch with async functions can happen. If you forget to catch errors, an async operation failure can crash your program.
function wait(ms) {
if (ms > 2015) throw new Error(ms);
return new Promise((r) => setTimeout(r, ms));
}
async function main() {
console.log('almost there...');
await wait(2007);
console.log('hold on...');
await wait(2012);
console.log('just a bit more...');
await wait(2016);
console.log('done!');
}
Losing parallelism
This is probably the biggest one. If you chain await sequentially, your program will run like a turtle. Each await blocks until that operation completes.
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function main() {
console.time('wait3s');
await wait(1000);
await wait(2000);
console.timeEnd('wait3s');
}
This takes a total of 1 + 2 = 3s to execute because you must wait for each wait call. How to avoid this? Start the async operations first, then collect the results later. Since Promise allows us to retrieve the result whenever it’s in a final state, we can fire them off early:
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function main() {
console.time('wait2s');
let w1 = wait(1000);
let w2 = wait(2000);
await w1;
await w2;
console.timeEnd('wait2s');
}
This only takes 2s because our wait calls run in parallel. Besides awaiting each Promise individually, we can use Promise.all to parallelize them:
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function main() {
console.time('wait2s');
await Promise.all([wait(1000), wait(2000)]);
console.timeEnd('wait2s');
}
You might think Promise.all and await-ing each Promise are the same, but they differ. Promise.all only succeeds when all passed Promises succeed; it fails as soon as any one fails. So if you want to tolerate individual failures, you can’t use Promise.all. You must use await with try catch for each Promise:
function wait(ms) {
if (ms > 2000) throw new Error(ms);
return new Promise((r) => setTimeout(r, ms));
}
async function main() {
const dur = [1000, 2000, 3000, 4000];
let all = dur.map((ms) => wait(ms));
try {
await Promise.all(all);
console.log('Promise.all - done');
} catch (e) {
console.error('Promise.all:', e);
}
let each = dur.map((ms) => wait(ms));
each.forEach(async (func, index) => {
try {
await func;
console.log('each - done:', dur[index]);
} catch (e) {
console.error('each:', e);
}
});
}
Platform/browser support
At the time of writing, the following platforms and browsers support async functions:
- Node.js v7.0 with
--harmony-async-awaitflag - Chrome v55
- Microsoft Edge v21.10547
If you need to run on unsupported platforms/browsers, you can use Babel:
Conclusion
The essence of async functions is Promise, so to use them, you need Promise for handling async operations. You can’t use await to wait for callback-based functions — you must wrap them in a Promise first.
Although async functions have very clear syntax, we still need to be careful about misusing keywords that cause bugs or obscure program logic. And especially watch out for accidentally destroying parallelism.
Given the convenience of async functions, we should adopt them now to reduce future maintenance burden. For platforms/browsers without native support, we can transpile with Babel.
Happy await-ing!
Comments