JavaScript supports three approaches to handling asynchronous operations.
The first approach involves using callback functions. In this approach, when an asynchronous operation completes, a callback function is invoked that was passed as an argument.
See the code below:
function callbackFn(){
//execute when the async operation completes
}
// pass callbackFn as an argument
asyncOperation(callbackFn)
Using this approach results in callback hell, which creates complex, difficult-to-read, and error-prone applications as you nest callbacks into callbacks.
To handle asynchronous operations effectively, JavaScript introduced promises
to solve the issue with callback hell.
A Promise
is an object that serves as a placeholder for the future result of an asynchronous operation and represents our operation's current state.
Upon completion of the asynchronous operation, we return a fulfilled promise object. In the promise object, we use the then()
method to utilize the result.
The syntax is as below:
const promise = asyncOperation()
promise.then(result => console.log(result))
Promises
introduce a better improvement in handling asynchronous operations and solved the challenge of callback hell.
The only problem is, Promises require a series of then()
to consume any returned Promise object, which increases the wordiness of our code.
async/await
was introduced in ES2017 as a better approach to handling Promises
In this post, we will learn how to use async/await
to utilize Promise
.
By the end of this article, you will learn:
- Why async/await is referred to as syntactic sugar
- How to declare an async function
- What is the async keyword
- What is the await keyword
- Difference between consuming Promises using .then() vs async/await
- The benefits of using async functions
- How to handle errors in an async function
Let's get started !
Understanding Syntactic Sugar
async/await
is referred to as syntactic sugar to Promise, but what does this really mean? Syntactic sugar is syntax within a programming language implemented to make code easier to read or express.
Rather than using Promise Chaining, we can use async/await to write well-structured code
The code below looks well-structured and devoid of wordiness.
async function getSomeData(){
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
const data = await response.json()
console.log(data)
}
//call the asynchronous function
getSomeData()
Understanding the Async keyword
The async
keyword prompts the JavaScript engine that, we are declaring an asynchronous function.
Using the async
keyword prior to a function automatically transforms it into a Promise
. Meaning, the return value of the async function will always be a Promise.
If the returned value of any async function is not clearly a promise, it would be silently wrapped in a promise
See the code below :
async function someAsyncOps(){
}
//call the asynchronous function
console.log(someAsyncOps())
See the code below:
async function someAsyncOps(){
}
//call the asynchronous function
console.log(someAsyncOps())
The output of the code will be
- There is no return value in the body of the function above. However, because of the async keyword used, the returned value was a
Promise
To reiterate, using the async
keyword prior to any function, the return value is a Promise
. This promise will be settled with the value returned by the async function or rejected with an exception thrown from the async function
Syntax of the Async function
We define the async function using either the regular function declaration or an arrow function.
The syntax of the async function is as below:
//function declaration
async function someAsyncOps(){
await ... //some async operation goes here
}
//arrow declaration
const someAsyncOps = async ()=>{
await ... //some async operation goes here
}
Understanding the Await keyword
When an await
expression is used, the execution of an async function is paused until a promise settles (fulfilled or rejected), returns the promised result, and then the execution resumes.
When resumed, the value of the await
expression is that of the fulfilled promise.
The await
keyword works only in the body of an async function. It prompts the JavaScript engine to wait for the asynchronous operation to complete before continuing with any statement below it in the function's body.
It works as a pause-until-done keyword, causing the JavaScript engine to pause code execution until the Promise is settled.
The syntax is as below:
//should be used inside an async function
let value = await promise //wait until promise is settled
Examine the code below:
async function someAsyncOps(){
//use Promise constructor
let promise = new Promise((resolve, reject)=>{
setTimeout(() => resolve('done'),4000)
});
let result = await promise; // (*) wait until promise settles
console.log(result)
console.log("Will resume execution when promise settled")
}
someAsyncOps()
In the code above:
The function execution "pauses" at the line (*) waiting for the promise to settle. Using the setTimeout to stimulate a delay, the promise takes about 4 seconds to settle. Once the promise settles, we return the fulfilled value to the result variable. After, any statement below that line of code will be executed.
To emphasize, the await hangs the function execution until the promise settles, and then resumes it with the promised result.
It is used as an alternative to promise handling rather than the .then()
method.
Inside the Async function's body
An async function's body can be thought of as being divided by zero or more await expressions.
Top-level code is executed synchronously and includes the first await expression (if there is one).
This way, an async function without an await expression will run synchronously. The async function will, however, always complete asynchronously if there is an await expression inside the function body
Run the code snippet below to understand
async function someAsyncOps(){
console.log("Top level code will execute first ") //executed synchronously
//use Promise constructor
let promise = new Promise((resolve, reject)=>{
setTimeout(() => resolve('done'),4000)
});
let result = await promise; //wait until promise settles, hence run asynchronousy
console.log(result)
console.log("Code below will resume execution now that the promise has settled")
}
//invoke the function
someAsyncOps()
The output will be
Benefits of Async/Await
With async function, the JavaScript engine will pause any code execution when it encounters the await expression, and only resumes when the promise is fulfilled.
The code below uses the async function:
async function someAsyncOps(){
console.log('Async/Await Readying...')
let response = await fetch('https://dummyjson.com/products/1')
let result = await response.json() // pause code execution until promise is fufilled
console.log(result)
console.log('This will only execute when the promise is fulfilled')
}
someAsyncOps()
The output will be:
However, the .then()
method does not pause any code execution. Code below the promise chaining will execute before the promise is fulfilled
The code below uses Promise chaining
function getData(){
console.log('Then method reading....')
fetch('https://dummyjson.com/products/1').then((res)=> console.log(res))
console.log('This will not pause, but will be executed before the promise is fulfilled');
}
getData()
The output will be
If we utilize promise chaining with then()
, we need to implement any logic you want to execute after the request in the promise chaining. Else like the example above, any code that you put after fetch() will execute immediately, before the fetch()
is done.
Async/Await
also makes it simple to convert code from synchronous procedure to asynchronous procedure.
Handling errors in an Async function
Error handling in async functions is very effortless. If the promise is rejected, we use the .catch()
method to handle it.
Because async functions return a promise, we can invoke the function, and append the .catch()
method to the end.
//handling errors in async functions
asyncFunctionCall().catch(err => {
console.error(err)
});
Similarly to synchronous code, if you want to handle the error directly inside the async function, we can use try/catch
.
The try...catch
statement is composed of a try
block and either a catch
block, a finally
block, or both. try
block code is executed first, and if it throws an exception, catch
block code is executed.
See the code below:
async function someAsyncOps(){
try {
let response = await fetch('https://jsonplaceholder.typicode.co') //api endpoint error
let result = await response.json()
console.log(result)
} catch (error) {
console.log("We got some error",error) //catches the error here
}
}
someAsyncOps()
The output of the code will be:
Summary
- Using
async
keyword prior to a function automatically transforms it into aPromise
. When an
await
expression is used, the execution of an async function is paused until a promise settles (fulfilled or rejected), returns its result, and then the execution resumes.Because the
await
is valid only inside async functions, the await expression does not block the main thread from executing.With
async/await
we rarely need to writepromise.then/catch
-These features make writing both readable and writable asynchronous code a breeze.
Please do comment or add your feedback, insight, or suggestion to this post. It helps clarify any concerns you might have and makes the article better.
If you have found value in this article, kindly share it on your social media platforms, and don't forget to connect with me on Twitter