Asynchronous Functions 101
One of the major advantages of JavaScript is that everything is asynchronous. For the most part, different parts of your code don’t affect the execution of others.
doALongThing(() => console.log("I will be logged second!"));
console.log("I will be logged first!");
Unfortunately, this is also one of JavaScript's major disadvantages. Because everything is asynchronous by default, it makes it that much more difficult when you do want to execute code synchronously.
The first solution to this problem was callbacks. If a part of your code was dependent on the result of something else, we would have to nest our code -
doSomething((response) => {
doSomethingElse(response,(secondResponse) => {
doAThirdThing(secondResponse);
});
})
Nesting callbacks on callbacks, as we know, became unmaintainable. So, the solution of promises was created. This allowed us to deal with synchronous code in a much cleaner, flatter, way.
doSomething()
.then((response) => doSomethingElse(response))
.then((secondResponse) => doAThirdThing(secondResponse));
// Even cleaner
doSomething().then(doSomethingElse).then(doAThirdThing);
As with everything, promises were not perfect either. So, as part of the ES2017 Specification, another method for dealing with synchronous code was defined; asynchronous functions. These allow us to write asynchronous code as if it were synchronous.
Creating an Asynchronous Function #
An asynchronous function is defined with the async
function expression. A basic function looks like this -
async function foo() {
const value = await somePromise();
return value;
}
We define a function as an asynchronous function by preceding the function declaration with async
. This keyword can be used with any function declaration syntax -
// Basic function
async function foo() { … }
// Arrow function
const foo = async () => { … }
// Class methods
class Bar {
async foo() { … }
}
Once we have defined a function as an asynchronous function, we are able to use the await
keyword. This keyword is placed before the calling of a promise, which will pause the execution of the function until the promise is either fulfilled or rejected.
Handling Errors #
Error handling in asynchronous functions is done with the help of try
and catch
blocks. The first block, try
, allows us to attempt an action. The second block, catch
, is called if the action is failed. It accepts one paramter containing whatever error was thrown.
async function foo() {
try {
const value = await somePromise();
return value;
}
catch (err) {
console.log("Oops, there was an error :(");
}
}
Using an Asynchronous Function #
Asynchronous functions aren't really a replacement for promises. The two work hand in hand. An asynchronous function will await
the execution of a promise, and an asynchronous function will always return a promise.
The promise returned by an asynchronous function will resolve with whatever value is returned by the function.
async function foo() {
await somePromise();
return ‘success!’
}
foo().then((res) => console.log(res)) // ‘success!’
If an error is thrown, the Promise will be rejected with that error.
async function foo() {
await somePromise();
throw Error(‘oops!’)
}
foo()
.then((res) => console.log(res))
.catch((err) => console.log(err)) // ‘oops!’
Executing Asynchronous Functions in Parallel #
With promises, we can execute multiple promises in parallel using the Promise.all()
method.
function pause500ms() {
return new Promise((res) => setTimeout(res, 500));
}
const promise1 = pause500ms();
const promise2 = pause500ms();
Promise.all([promise1, promise2]).then(() => {
console.log("I will be logged after 500ms");
});
With asynchronous functions, we have to work around a bit to get the same effect. If we just list each function to await in sequence, they will be executed in sequence since await
will pause the execution of the rest of the function.
async function inSequence() {
await pause500ms();
await pause500ms();
console.log("I will be logged after 1000ms");
}
This will take 1000ms to complete, because the second await
is not even started until the first has been completed. To get around this, we need to reference the functions in this way -
async function inParallel() {
const await1 = pause500ms();
const await2 = pause500ms();
await await1;
await await2;
console.log("I will be logged after 500ms");
}
This will take only 500ms to complete because both pause500ms()
functions are being executed at the same time.
Promises or Asynchronous Function? #
As I mentioned, asynchronous functions aren't a replacement for promises, the two are used together. Asynchronous functions provide an alternative, and in some cases better, way of working with promise-based functions. But, they still use and produce promises.
Since a promise is returned, an asynchronous function can be called by another asynchronous function, or a promise. We can mix and match depending on which syntax is best suited for each case.
function baz() {
return new Promise((res) => setTimeout(res, 1000));
}
async function foo() {
await baz();
return 'foo complete!';
}
async function bar() {
const value = await foo();
console.log(value);
return 'bar complete!';
}
bar().then((value) => console.log(value));
The following will occur:
- Wait 1000ms
- Log "foo complete!"
- Log "bar complete!"
Support #
At the time of writing, both asynchronous functions and promises are available in the current versions of all major browsers, with the exception of Internet Explorer and Opera Mini.