
Async / await: 6 reasons to forget about promises
- Transfer
If you're not up to date, starting with version 7.6, Node.js has built-in support for the async / await mechanism. They have been talking about it, of course, for a long time, but it is one thing when “crutches” are needed to use some functionality, and it’s quite another when all this goes, as they say, “out of the box”. If you have not tried async / await yet, be sure to try it.

Today we will consider six features of async / await, which allow us to classify a new approach to writing asynchronous code as tools that should be mastered and used wherever possible, replacing them with what was before.
For those not familiar with async / await, here are some basic things you should know before moving on.
Imagine that we have a function
Using promises, this can be done like this:
Here's how the same thing is done using async / await:
If you compare the two above code snippets, you can find the following:
Consider the promised six benefits of async / await over traditional promises.
Comparing the two examples above, notice how shorter the one where async / await is used. And in fact, in this case, we are talking about small pieces of code, and if we talk about real programs, the savings will be even greater. The thing is that you don’t need to write
The async / await design finally made it possible to handle synchronous and asynchronous errors using the same mechanism - the good old
Here is the same rewritten using async / await. The block
Imagine writing a piece of code that, after downloading some data, decides whether to return it to the dial-peer, or, based on what has already been received, request something else. You can solve a similar problem like this:
Only by looking at this design can your head hurt. It is very easy to get lost in nested constructions (there are 6 levels here), brackets, return commands, which are needed only to deliver the final result to the main promise.
The code will be much easier to read if you solve the problem using async / await.
You may have encountered a situation where you call
If you
With this approach, semantics are sacrificed for code readability. There is no reason to put
The same can be written using async / await, and this is done surprisingly simply, but what happens is intuitive. Here you will involuntarily think about how much useful can be done for the time that is spent on writing at least some decent code using promises.
Imagine a piece of code in which there is a chain of promise calls, and somewhere in this chain an error is thrown.
The error stack returned from the promise chain does not indicate where exactly the error occurred. Moreover, the error message can direct efforts to find the problem in the wrong way. The only function name that is contained in the message is
If you look at a similar situation when using async / await, the error stack will indicate the function in which the problem occurred.
This can not be called a huge plus, if we are talking about development in the local environment, when the file with the code is opened in the editor. But this turns out to be very useful if you are trying to understand the cause of the error by analyzing the log file received from the production server. In such cases, to know that the error occurred in
This item is the last, but this does not mean that it is not particularly important. The most valuable feature of using async / await is that the code involved in this construct is much easier to debug. Debugging promises has always been a nightmare for two reasons.
1. You cannot set a breakpoint in arrow functions that return the results of expressions (there is no function body).

Try setting breakpoint
2 somewhere in this code . If you set a breakpoint inside the block
When using async / await, there is no special need for arrow functions, and you can "step" on calls made with the keyword

Debugging using async / await
It is possible that you will have some considerations not in favor of using async / await, dictated by healthy skepticism. Let's comment on a couple of the most likely.
Perhaps async / await is one of the most useful revolutionary features added to JavaScript in the last few years. It provides simple and convenient ways to write and debug asynchronous code. We believe that async / await is useful to anyone who has to write such code.
Dear readers! How do you feel about async / await? Do you use this opportunity in your projects?

Today we will consider six features of async / await, which allow us to classify a new approach to writing asynchronous code as tools that should be mastered and used wherever possible, replacing them with what was before.
Async / await basics
For those not familiar with async / await, here are some basic things you should know before moving on.
- Async / await is a new way to write asynchronous code. Previously, a similar code was written using callbacks and promises.
- Our proposal to “forget about promises” does not mean that they have lost relevance in the light of new technology. In fact, async / await is based on promises. Keep in mind that this mechanism cannot be used with callbacks.
- Constructs built using async / await, like promises, do not block the main thread of program execution.
- Thanks to async / await, asynchronous code becomes similar to synchronous, and in its behavior features of such a code appear, which are very useful in some situations where it was inconvenient to use promises for various reasons. But before they could not do without them. Now everything has changed. This is where the power of async / await lies.
Syntax
Imagine that we have a function
getJSON
that returns a promise, upon successful resolution of which a JSON object is returned. We want to call this function, output a JSON object to the log and return it done
. Using promises, this can be done like this:
const makeRequest = () =>
getJSON()
.then(data => {
console.log(data)
return "done"
})
makeRequest()
Here's how the same thing is done using async / await:
const makeRequest = async () => {
console.log(await getJSON())
return "done"
}
makeRequest()
If you compare the two above code snippets, you can find the following:
- When describing the function, the keyword is used
async
. The keywordawait
can only be used in functions defined usingasync
. Any such function implicitly returns a promise, and the value returned when resolving this promise will be what the instruction returnsreturn
, in our case this is a stringdone
. - The above implies that the keyword
await
cannot be used outside of async functions, at the top level of the code.// Эта конструкция на верхнем уровне кода работать не будет // await makeRequest() // А такая - будет makeRequest().then((result) => { // do something })
- Recording
await getJSON()
means that the callconsole.log
will wait for the promise to resolvegetJSON()
, after which it will output what the function returns.
Why is async / await better than promises?
Consider the promised six benefits of async / await over traditional promises.
▍1. Concise and clean code
Comparing the two examples above, notice how shorter the one where async / await is used. And in fact, in this case, we are talking about small pieces of code, and if we talk about real programs, the savings will be even greater. The thing is that you don’t need to write
.then
, create an anonymous function to process the response, or include in the code a variable with a name data
that we, in fact, do not need. In addition, we got rid of nested structures. The usefulness of these minor improvements will become more apparent when we look at other examples.▍2. Error processing
The async / await design finally made it possible to handle synchronous and asynchronous errors using the same mechanism - the good old
try/catch
. In the following example with promises, it try/catch
will not handle the failure that occurred during the call JSON.parse
, since it is executed inside the promise. To handle such an error, you need to call the .catch()
Promis method and duplicate the error handling code in it. In the code of a working project, error handling will be clearly more difficult than calling console.log
from an example.const makeRequest = () => {
try {
getJSON()
.then(result => {
// парсинг JSON может вызвать ошибку
const data = JSON.parse(result)
console.log(data)
})
// раскомментируйте этот блок для обработки асинхронных ошибок
// .catch((err) => {
// console.log(err)
// })
} catch (err) {
console.log(err)
}
Here is the same rewritten using async / await. The block
catch
will now also handle errors that occurred when parsing JSON.const makeRequest = async () => {
try {
// парсинг JSON может вызвать ошибку
const data = JSON.parse(await getJSON())
console.log(data)
} catch (err) {
console.log(err)
}
}
▍3. Check conditions and perform asynchronous actions
Imagine writing a piece of code that, after downloading some data, decides whether to return it to the dial-peer, or, based on what has already been received, request something else. You can solve a similar problem like this:
const makeRequest = () => {
return getJSON()
.then(data => {
if (data.needsAnotherRequest) {
return makeAnotherRequest(data)
.then(moreData => {
console.log(moreData)
return moreData
})
} else {
console.log(data)
return data
}
})
}
Only by looking at this design can your head hurt. It is very easy to get lost in nested constructions (there are 6 levels here), brackets, return commands, which are needed only to deliver the final result to the main promise.
The code will be much easier to read if you solve the problem using async / await.
const makeRequest = async () => {
const data = await getJSON()
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data);
console.log(moreData)
return moreData
} else {
console.log(data)
return data
}
}
▍4. Intermediate values
You may have encountered a situation where you call
promise1
, then use what it returns to call promise2
, then use both results from the previously called promise for the call promise3
. Here is the code that solves such a problem.const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return promise2(value1)
.then(value2 => {
// do something
return promise3(value1, value2)
})
})
}
If you
promise3
don’t need it value1
, you can easily simplify the program structure, especially if you are struck by similar constructions that you used unnecessarily. In such a situation can be wrapped value1
and value2
in the call Promise.all
and avoid unnecessary nested structures.const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return Promise.all([value1, promise2(value1)])
})
.then(([value1, value2]) => {
// do something
return promise3(value1, value2)
})
}
With this approach, semantics are sacrificed for code readability. There is no reason to put
value1
and value2
in the same array, except to avoid nesting PROMIS. The same can be written using async / await, and this is done surprisingly simply, but what happens is intuitive. Here you will involuntarily think about how much useful can be done for the time that is spent on writing at least some decent code using promises.
const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}
▍5. Error stacks
Imagine a piece of code in which there is a chain of promise calls, and somewhere in this chain an error is thrown.
const makeRequest = () => {
return callAPromise()
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => {
throw new Error("oops");
})
}
makeRequest()
.catch(err => {
console.log(err);
// вывод
// Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
The error stack returned from the promise chain does not indicate where exactly the error occurred. Moreover, the error message can direct efforts to find the problem in the wrong way. The only function name that is contained in the message is
callAPromise
, and this function does not cause any errors (although, of course, there is also useful information here - information about the file where the error occurred and about the line number). If you look at a similar situation when using async / await, the error stack will indicate the function in which the problem occurred.
const makeRequest = async () => {
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
throw new Error("oops");
}
makeRequest()
.catch(err => {
console.log(err);
// вывод
// Error: oops at makeRequest (index.js:7:9)
})
This can not be called a huge plus, if we are talking about development in the local environment, when the file with the code is opened in the editor. But this turns out to be very useful if you are trying to understand the cause of the error by analyzing the log file received from the production server. In such cases, to know that the error occurred in
makeRequest
is better than to know that the source of the error is a certain one then
, caused after another one then
, which follows another then
...▍6. Debugging
This item is the last, but this does not mean that it is not particularly important. The most valuable feature of using async / await is that the code involved in this construct is much easier to debug. Debugging promises has always been a nightmare for two reasons.
1. You cannot set a breakpoint in arrow functions that return the results of expressions (there is no function body).

Try setting breakpoint
2 somewhere in this code . If you set a breakpoint inside the block
.then
and use a debugger command such as “step-by-step”, the debugger will not go to the next one .then
, since it can only “step over” through synchronous blocks of code. When using async / await, there is no special need for arrow functions, and you can "step" on calls made with the keyword
await
in the same way as with regular synchronous calls.
Debugging using async / await
Remarks
It is possible that you will have some considerations not in favor of using async / await, dictated by healthy skepticism. Let's comment on a couple of the most likely.
- This design makes asynchronous calls less obvious. Yes, it is, and someone who is accustomed to seeing asynchrony where there is a callback or
.then
can spend several weeks adjusting themselves to automatically comprehending async / await instructions. However, for example, in C # there has been similar functionality for many years, and those who are familiar with this know that the benefit of it is worth the temporary inconvenience when reading code. - Node 7 is not an LTS release. This is true, but Node 8 will be released very soon, and translating the code to the new version will probably not require much effort.
conclusions
Perhaps async / await is one of the most useful revolutionary features added to JavaScript in the last few years. It provides simple and convenient ways to write and debug asynchronous code. We believe that async / await is useful to anyone who has to write such code.
Dear readers! How do you feel about async / await? Do you use this opportunity in your projects?