In JavaScript, the async/await
syntax is a modern approach to handling asynchronous operations. It provides a more concise and readable way to write asynchronous code compared to using callbacks or promises. The async
keyword is used to define an asynchronous function, while the await
keyword is used to pause the execution of the function until a promise is resolved. This section will introduce you to the async/await
syntax in JavaScript and demonstrate how it can simplify working with promises.
The general syntax for an async
function is as follows:
async function myAsyncFunction() {
// Asynchronous code here
}
The async
keyword before a function declaration indicates that the function will return a promise. Inside an async
function, you can use the await
keyword to pause the execution of the function until a promise is resolved. The await
keyword can only be used inside an async
function.
The general syntax for using the await
keyword is as follows:
async function myAsyncFunction() {
const result = await someFunctionThatReturnsPromise;
// Code that runs after the promise is resolved
}
When the await
keyword is used, the function execution is paused until the promise is resolved. The value of the promise is stored in the result
variable, which can then be used in the subsequent code.
Let’s see an example of using async/await
to fetch data from a server:
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}
In the example above, the fetchData
function uses the fetch
API to make a network request to https://api.example.com/data
. The await
keyword is used to pause the function execution until the promise returned by fetch
is resolved. Once the promise is resolved, the response is converted to JSON using the response.json()
method, and the data is returned.
It is common to use try/catch
blocks with async/await
to handle errors that may occur during the execution of asynchronous code. Here’s an example:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
// Handle errors, e.g., log the error or throw a new error, etc.
console.error('An error occurred:', error);
throw new Error('Failed to fetch data');
}
}
In the example above, the try
block contains the asynchronous code that may throw an error. If an error occurs during the execution of the fetchData
function, the catch
block will handle the error. You can log the error, throw a new error, or perform any other error handling logic inside the catch
block.
Let’s refactor the example from the previous section to use async/await along with promises. Here are our three functions getId
, getCourses
, and getGrades
that return promises:
function getId(student) {
return new Promise((resolve, reject) => {
let success, id;
console.log(`Fetching ${student} info!`);
setTimeout(() => {
id = "jdoe23";
success = true;
if (success) {
console.log(`Received ${student} info!`);
resolve(id);
} else {
reject(new Error(`Unable to fetch ${student} info`));
}
}, 5000);
});
}
function getCourses(student_id) {
return new Promise((resolve, reject) => {
let success, courses;
console.log(`Fetching ${student_id}'s courses!`);
setTimeout(() => {
success = true;
courses = ["course-1", "course-2"];
console.log(`Received ${student_id}'s courses!`);
if (success) {
resolve({ student_id, courses }); // Updated to return student_id too!
} else {
reject(new Error(`Unable to fetch ${student_id}'s courses`));
}
}, 5000);
});
}
function getGrades(student_id, student_courses) {
return new Promise((resolve, reject) => {
let success, grades;
console.log(`Fetching ${student_id}'s grades!`);
setTimeout(() => {
success = true;
grades = student_courses.map((course) => {
return { course: course, grade: Math.floor(Math.random() * 100) };
});
console.log(`Received ${student_id}'s grades!`);
if (success) {
resolve(grades);
} else {
reject(new Error(`Unable to fetch ${student_id}'s grades`));
}
}, 5000);
});
}
Here is that code that consumes these functions:
console.log("Listening to events!");
getId("John Doe")
.then((id) => {
console.log("John Doe ID:", id);
return getCourses(id);
})
.then(({ id, courses }) => {
console.log("John Doe Courses:", courses);
return getGrades(id, courses);
})
.then((grades) => {
console.log("John Doe Grades:", grades);
})
.catch((error) => {
console.error(error);
});
console.log("Still listening to events!");
We can refactor the code above to use async/await
as follows:
async function displayStudentInfo() {
try {
const id = await getId("John Doe");
console.log("John Doe ID:", id);
const { student_id, courses } = await getCourses(id);
console.log("John Doe Courses:", courses);
const grades = await getGrades(student_id, courses);
console.log("John Doe Grades:", grades);
} catch (error) {
console.error(error);
}
}
console.log("Listening to events!");
displayStudentInfo();
console.log("Still listening to events!");
In the refactored code, the displayStudentInfo
function is defined as an async
function. Inside this function, we use await
to call the getId
, getCourses
, and getGrades
functions sequentially. The try/catch
block is used to handle any errors that may occur during the execution of these functions. Running the code should result in the same output as before.
Listening to events!
Fetching John Doe info!
Still listening to events!
Received John Doe info!
John Doe ID: jdoe23
Fetching jdoe23's courses!
Received jdoe23's courses!
John Doe Courses: [ 'course-1', 'course-2' ]
Fetching undefined's grades!
Received undefined's grades!
John Doe Grades: [
{ course: 'course-1', grade: 99 },
{ course: 'course-2', grade: 57 }
]
You might be wondering why we needed to define the displayStudentInfo
function instead of directly using async/await
in the global scope. The reason is that top-level await
is not allowed in JavaScript modules. Therefore, we need to wrap the await
calls inside an async
function to use async/await
syntax.
Arguably, the async/await
syntax makes the code more readable and easier to follow compared to using promises and chaining .then
and .catch
blocks. It simplifies the process of writing asynchronous code and handling errors, making it a powerful tool for managing asynchronous operations in JavaScript. By using async/await
, you can write cleaner and more maintainable code that is easier to reason about.
To gain more practice with async/await
, we can refactor the getId
, getCourses
, and getGrades
functions to use async/await
as well. This will further demonstrate how async/await
can simplify working with promises and make the code more readable and concise.
// Generic async work function to simulate network requests
function asyncWork(data, delay = 5000) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(data);
}, delay);
});
}
// Refactored functions using asyncWork
async function getId(student) {
console.log(`Fetching ${student} info!`);
const id = await asyncWork("jdoe23");
console.log(`Received ${student} info!`);
return id;
}
async function getCourses(student_id) {
console.log(`Fetching ${student_id}'s courses!`);
const courses = await asyncWork(["course-1", "course-2"]);
console.log(`Received ${student_id}'s courses!`);
return { student_id, courses };
}
async function getGrades(student_id, student_courses) {
console.log(`Fetching ${student_id}'s grades!`);
const grades = await asyncWork(
student_courses.map((course) => ({
course: course,
grade: Math.floor(Math.random() * 100),
}))
);
console.log(`Received ${student_id}'s grades!`);
return grades;
}
async function displayStudentInfo() {
try {
const id = await getId("John Doe");
console.log("John Doe ID:", id);
const { student_id, courses } = await getCourses(id);
console.log("John Doe Courses:", courses);
const grades = await getGrades(student_id, courses);
console.log("John Doe Grades:", grades);
} catch (error) {
console.error(error);
}
}
console.log("Listening to events!");
displayStudentInfo();
console.log("Still listening to events!");
In the refactored code, the getId
, getCourses
, and getGrades
functions are rewritten to use the async/await
syntax with the asyncWork
function that simulates network requests. The functions now directly return the resolved values instead of using promises. The displayStudentInfo
function remains the same and uses async/await
to call the refactored functions.
The async/await syntax simplifies the code by removing the need for explicit promise handling and chaining. It allows you to write asynchronous code that closely resembles synchronous code, making it easier to understand and maintain. By using async/await, you can write cleaner and more readable code that is easier to reason about and debug. It might be interesting to know that async/await is syntax sugar over promises, which means that it is built on top of promises to provide a more intuitive way to work with asynchronous code.