In JavaScript, there are two main ways to handle asynchronous code:
then/catch
(ES6) andasync/await
(ES7). These syntaxes give us the same underlying functionality, but they affect readability and scope in different ways. In this article, we’ll see how one syntax lends itself to maintainable code, while the other puts us on the road to callback hell!
JavaScript runs code line by line, moving to the next line of code only after the previous one has been executed. But executing code like this can only take us so far. Sometimes, we need to perform tasks that take a long or unpredictable amount of time to complete: fetching data or triggering side-effects via an API, for example.
Rather than letting these tasks block JavaScript’s main thread, the language allows us to run certain tasks in parallel. ES6 saw the introduction of the Promise object as well as new methods to handle the execution of these Promises: then
, catch
, and finally
. But a year later, in ES7, the language added another approach and two new keywords: async
and await
.
This article isn’t an explainer of asynchronous JavaScript; there are lots of good resources available for that. Instead, it addresses a less-covered topic: which syntax — then/catch
or async/await
— is better? In my view, unless a library or legacy codebase forces you to use then/catch
, the better choice for readability and maintainability is async/await
. To demonstrate that, we’ll use both syntaxes to solve the same problem. By slightly changing the requirements, it should become clear which approach is easier to tweak and maintain.
We’ll start by recapping the main features of each syntax, before moving to our example scenario.
then
, catch
And finally
then
and catch
and finally
are methods of the Promise object, and they are chained one after the other. Each takes a callback function as its argument and returns a Promise.
For example, let’s instantiate a simple Promise:
const greeting = new Promise((resolve, reject) => {
resolve("Hello!");
});
Using then
, catch
and finally
, we could perform a series of actions based on whether the Promise is resolved (then
) or rejected (catch
) — while finally
allows us to execute code once the Promise is settled, regardless of whether it was resolved or rejected:
greeting
.then((value) => {
console.log("The Promise is resolved!", value);
})
.catch((error) => {
console.error("The Promise is rejected!", error);
})
.finally(() => {
console.log(
"The Promise is settled, meaning it has been resolved or rejected."
);
});
For the purposes of this article, we only need to use then
. Chaining multiple then
methods allows us to perform successive operations on a resolved Promise. For example, a typical pattern for fetching data with then
might look something like this:
fetch(url)
.then((response) => response.json())
.then((data) => {
return {
data: data,
status: response.status,
};
})
.then((res) => {
console.log(res.data, res.status);
});
async
And await
By contrast, async
and await
are keywords which make synchronous-looking code asynchronous. We use async
when defining a function to signify that it returns a Promise. Notice how the placement of the async
keyword depends on whether we’re using regular functions or arrow functions:
async function doSomethingAsynchronous() {
// logic
}
const doSomethingAsynchronous = async () => {
// logic
};
await
, meanwhile, is used before a Promise. It pauses the execution of an asynchronous function until the Promise is resolved. For example, to await our greeting
above, we could write:
async function doSomethingAsynchronous() {
const value = await greeting;
}
We can then use our value
variable as if it were part of normal synchronous code.
As for error handling, we can wrap any asynchronous code inside a try...catch...finally
statement, like so:
async function doSomethingAsynchronous() {
try {
const value = await greeting;
console.log("The Promise is resolved!", value);
} catch (e) {
console.error("The Promise is rejected!", error);
} finally {
console.log(
"The Promise is settled, meaning it has been resolved or rejected."
);
}
}
Finally, when returning a Promise inside an async
function, you don’t need to use await
. So the following is acceptable syntax.
async function getGreeting() {
return greeting;
}
However, there’s one exception to this rule: you do need to write return await
if you’re looking to handle the Promise being rejected in a try...catch
block.
async function getGreeting() {
try {
return await greeting;
} catch (e) {
console.error(e);
}
}
Using abstract examples might help us understand each syntax, but it’s difficult to see why one might be preferable to the other until we jump into an example.
#javascript #programming #web-development #developer