JavaScript is a single-threaded and non-blocking language. This means that many operations, such as fetching data from an API or reading a file, run asynchronously — they don't wait for one operation to finish before moving on to the next. We used to handle this with nested callbacks, then with Promises. Now, async/await has arrived as the most elegant way to write asynchronous code.
What Is Async/Await?
Async/await is a syntax introduced in ES2017 (ES8) that is built on top of Promises. It doesn't replace Promises; it simply makes them easier to write and read. Code that used to be full of chained .then() calls can now be written just like ordinary synchronous code.
How to Create an Async Function
Add the async keyword before a function, and that function will automatically return a Promise:
// A simple async function
async function goodMorning() {
return 'Good Morning!';
}
// The result is a Promise, not a string directly
goodMorning().then(message => console.log(message)); // 'Good Morning!'
// It can also be written as an arrow function
const goodNight = async () => {
return 'Good Night!';
};
Using await
The await keyword can only be used inside a function marked as async. It will "wait" for a Promise to complete before continuing execution:
function waitTwoSeconds() {
return new Promise(resolve => setTimeout(resolve, 2000));
}
async function run() {
console.log('Starting...');
await waitTwoSeconds(); // Wait 2 seconds
console.log('Done after 2 seconds!');
}
run();
Async/Await with the Fetch API
The most common use of async/await is when fetching data from an API:
async function getUser(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
const user = await response.json();
return user;
}
async function showUser() {
const user = await getUser(1);
console.log('Name:', user.name);
console.log('Email:', user.email);
console.log('City:', user.address.city);
}
showUser();
Handling Errors with try/catch
A major advantage of async/await is the ability to handle errors with the familiar try/catch block, instead of using .catch() which is sometimes forgotten:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
// Manually throw an error if the HTTP status is not 2xx
throw new Error(`Failed to fetch data. Status: ${response.status}`);
}
const data = await response.json();
console.log('Data loaded:', data);
return data;
} catch (error) {
// Catches network errors OR the error we throw above
console.error('An error occurred:', error.message);
return null; // Return null so the program doesn't crash
} finally {
// This block ALWAYS runs, whether it succeeds or not
console.log('Data fetching process finished.');
}
}
fetchData('https://jsonplaceholder.typicode.com/posts/1');
Running Multiple Async Operations at Once
Sometimes we need to wait for several async operations at the same time. Use Promise.all() so they all run in parallel rather than one after another:
async function fetchAll() {
// WRONG — runs sequentially, slower
// const posts = await fetch('/api/posts').then(r => r.json());
// const users = await fetch('/api/users').then(r => r.json());
// CORRECT — runs in parallel, faster
const [postResponse, userResponse] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts?_limit=3'),
fetch('https://jsonplaceholder.typicode.com/users?_limit=3')
]);
const posts = await postResponse.json();
const users = await userResponse.json();
console.log('Posts:', posts.length);
console.log('Users:', users.length);
}
fetchAll();
Comparison: Promise .then() vs Async/Await
See how much cleaner the code is with async/await:
// With Promise .then() — lots of callbacks
function promiseVersion() {
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(res => res.json())
.then(post => {
return fetch(`https://jsonplaceholder.typicode.com/users/${post.userId}`);
})
.then(res => res.json())
.then(user => {
console.log('Author:', user.name);
})
.catch(err => console.error(err));
}
// With async/await — far easier to read
async function asyncVersion() {
try {
const postRes = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const post = await postRes.json();
const userRes = await fetch(`https://jsonplaceholder.typicode.com/users/${post.userId}`);
const user = await userRes.json();
console.log('Author:', user.name);
} catch (err) {
console.error(err);
}
}
Conclusion
Async/await is the best way to write asynchronous code in modern JavaScript. It makes the logic flow easier to read like synchronous code, error handling more intuitive with try/catch, and debugging simpler. Always be sure to add error handling so your application doesn't crash when a network issue or unexpected data occurs.