How to effectively utilize Promise in JavaScript using Async/Await
Table of contents
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()
vsasync/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())
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
Does async/await block the main thread?
The await
keyword only stops code execution declared within the async function. It ensures the next lines of code are executed when the promise is settled.
Because the await
is valid only inside async functions, the await
expression does not block the main thread from executing.
It only defers the execution of code that depends on the result.
Examine the code below:
async function someAsyncOps(){
let response = await fetch('https://dummyjson.com/products/1'); //pause until promise settles
let result = await response.json() // pause until promise settles
console.log(result)
console.log(`finally, we get to use the description: ${result.description}`)
}
console.log("Hello will execute first") // *
someAsyncOps()
console.log("I will execute next") // **
Let's examine the code above to ascertain whether async/ await blocks the main thread from execution. When the code is run:
- The line marked (*) will execute first
- Because
someAsyncOps()
is an asynchronous function, it will run in the background and will only execute when the promise settles. - The JavaScript engine moves to the line marked (**) and executes that line of code
- Because there is no code execution left, we return to the execution of the
someAsyncOps
- In the body of
someAsyncOps()
function, thefetch()
method is used to request data from an API endpoint. - The
await
keyword used prior to thefetch()
will cause the execution of any code below it to pause - Once the promise settles and it returns its value, the execution resumes
- Only then do we have the final line of code in the async function run.
The final output will be:
Consuming Promises using .then() versus the async/await
The code below illustrates how to handle promises using the .then()
method.
function someAsyncOps(){
return (
//using the fetch API to request for data
fetch('https://jsonplaceholder.typicode.com/users').then(res => res.json()).then(res => console.log(res))
)
}
someAsyncOps()
In the code above:
- We use the
fetch()
API to request for users using the JSONPlaceholder API endpoint. - The
fetch()
returns a Promise, and we make use of the data in the settled Promise by chaining a series of.then()
methods.
Let's see how to handle the promise using async/await
async function someAsyncOps(){
let response = await fetch('https://jsonplaceholder.typicode.com/users')
let result = await response.json()
console.log(result)
}
someAsyncOps()
In the code above:
- We prepend the function declaration with the
async
keyword. This prompts the JavaScript engine we are dealing with an asynchronous operation. - We use the
await
keyword prior to thefetch()
method - The
fetch()
method performs an asynchronous task, and when the Promise settles it returns a response. - As long as there is the
await
keyword, the JavaScript engine will pause the execution of any code below it until the Promise is settled. - The promise response can be read using the
.json()
method - Because the return value of the
.json()
method is a Promise, we prepend theresponse.json()
with anotherawait
keyword to pause the execution of any code below until the Promise settles. - The fulfilled promise is then assigned to the result variable.
- We finally console.log(result).
With async/await
we rarely need to write promise.then/catch
, instead of chaining a lot of .then()
methods, we replace every .then()
with await
to make the code simple to understand, execute, and maintain.
The output of both codes 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:
To summarize
- Using
async
keyword prior to a function automatically transforms it into a Promise. - 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 write
promise.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