Async / Await in JavaScript
Async/await is how modern JavaScript handles waiting for things like API calls and database queries. It makes asynchronous code read like normal code. If you've struggled with Promises and .then() chains, this is the solution.
Async/await is syntax for working with Promises and asynchronous operations such as calling an API, reading a file, or waiting for a timer. An async function always returns a Promise. await pauses the surrounding async function until a Promise settles, without blocking the JavaScript thread or preventing other work from continuing.
Start here: async/await is built on Promises, so learn how a Promise resolves or rejects before continuing. Read the Promises tutorial first, then return here for the cleaner syntax.
Before async/await, developers mostly used Promises with .then() and .catch(). Async/await uses the same Promise system, but the code reads more like normal step-by-step logic.
// Without async/await (using .then)
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then(response => response.json())
.then(data => console.log(data.title))
.catch(err => console.error(err));
// With async/await (same thing, easier to read)
async function getPost() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const data = await response.json();
console.log(data.title);
} catch (err) {
console.error(err);
}
}
getPost();
Both examples do the same thing. But the async/await version reads top to bottom, just like normal code. No nested callbacks. No chain of .then(). Much easier to follow.
When you put async in front of a function, two things happen:
- The function always returns a Promise, even if you return a plain value
- You can use the await keyword inside that function
async function greet() {
return "Hello!";
}
// Even though we returned a plain string,
// the function returns a Promise
greet().then(message => console.log(message)); // Hello!
You can use async with any type of function:
// Regular function
async function greet() {
return "Hello!";
}
// Arrow function
const greetArrow = async () => {
return "Hello!";
};
// Object method
const user = {
getRole: async function() {
return "admin";
}
};
// Class method
class UserService {
async getUser() {
return { role: "admin" };
}
}
await pauses the execution of the surrounding async function until the Promise settles. If it fulfills, await evaluates to the fulfilled value. If it rejects, await throws the rejection reason. This pause does not block the JavaScript thread or the rest of the application.
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function run() {
console.log("Start");
await wait(2000); // pauses here for 2 seconds
console.log("Done after 2 seconds");
}
run();
console.log("This runs immediately - not blocked");
You can use await inside an async function. Modern JavaScript also allows await at the top level of an ES module; in a classic script or regular function, using it outside those contexts causes a syntax error.
async function loadData() {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const data = await response.json();
console.log(data.title); // delectus aut autem
console.log(data.completed); // false
}
loadData();
A common mistake is forgetting await. In that case, the variable contains a Promise instead of the resolved value. A Promise is truthy, so a check such as if (data) can pass even when data is not the data you expected.
When a Promise rejects (something goes wrong), await throws an error. To handle it, wrap your await calls inside a try/catch block.
try/catch acts as a safety net. You try to do something that might fail - if it does, the catch block runs, and your app does not crash.
async function getUser(id) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) {
throw new Error("User not found");
}
const user = await response.json();
console.log(user.name);
} catch (error) {
console.error("Something went wrong:", error.message);
}
}
getUser(1); // Leanne Graham
getUser(999); // Something went wrong: User not found
You can also add a finally block for code that should always run, no matter what happens - like hiding a loading spinner:
async function loadData() {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const data = await res.json();
console.log(data.title);
} catch (err) {
console.error("Failed to load:", err.message);
} finally {
console.log("Loading finished"); // always runs
}
}
loadData();
Use the right error pattern: check response.ok for an HTTP error, use catch for network or rejected-Promise errors, and use finally to reset loading state. Do not show a successful empty page when a request failed.
async function loadProfile() {
const status = document.querySelector("#status");
status.textContent = "Loading...";
try {
const response = await fetch("/api/profile");
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
const profile = await response.json();
status.textContent = profile.name;
} catch (error) {
status.textContent = "Could not load your profile. Try again.";
console.error(error);
} finally {
document.querySelector("#retry-button").disabled = false;
}
}
This pattern separates user feedback from developer diagnostics: the page gets a helpful message, while the console keeps the technical error for debugging.
Async/await does not replace Promises - it is built on top of them. Under the hood, an async function returns a Promise. await pauses the surrounding async function until that Promise settles. You are writing the same asynchronous logic with different syntax.
// Using Promises (.then chains)
function getPost() {
return fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(res => res.json())
.then(data => {
console.log(data.title);
})
.catch(err => console.error(err));
}
// Same thing using async/await
async function getPost() {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const data = await res.json();
console.log(data.title);
} catch (err) {
console.error(err);
}
}
Both versions produce the same result. Use async/await when your code has multiple steps that depend on each other. Use Promise.all() when you want to start multiple independent asynchronous operations concurrently and wait for all of them to complete:
// Running multiple independent tasks concurrently
async function loadDashboard() {
const [users, posts] = await Promise.all([
fetch("https://jsonplaceholder.typicode.com/users").then(r => r.json()),
fetch("https://jsonplaceholder.typicode.com/posts").then(r => r.json())
]);
console.log("Users:", users.length); // 10
console.log("Posts:", posts.length); // 100
}
loadDashboard();
Important: start all independent requests before awaiting them. Awaiting the first request before creating the second one makes them run one after another.
async function loadDashboard() {
const requests = [
fetch("https://jsonplaceholder.typicode.com/users").then(checkResponse),
fetch("https://jsonplaceholder.typicode.com/posts").then(checkResponse)
];
try {
const [usersResponse, postsResponse] = await Promise.all(requests);
const [users, posts] = await Promise.all([
usersResponse.json(),
postsResponse.json()
]);
return { users, posts };
} catch (error) {
console.error("Dashboard could not load:", error.message);
return null;
}
}
function checkResponse(response) {
if (!response.ok) {
throw new Error(`Request failed with ${response.status}`);
}
return response;
}
Promise.all() rejects as soon as one request rejects. That is useful when the page needs every piece of data, but it is not ideal when one failed panel should not hide the others.
const results = await Promise.allSettled([
fetch("/api/weather").then(checkResponse),
fetch("/api/news").then(checkResponse)
]);
for (const result of results) {
if (result.status === "fulfilled") {
console.log("Panel loaded", result.value);
} else {
console.error("One panel failed", result.reason);
}
}
Choose deliberately: use Promise.all() when all results are required; use Promise.allSettled() when each result can succeed or fail independently and you want to render partial results.
Guideline: Use async/await when it makes asynchronous control flow easier to read. .then() and .catch() are still valid and useful, especially for short Promise chains or when an async function is not needed.
- async makes a function always return a Promise
- await pauses the surrounding async function until a Promise settles and gives you its fulfillment value when successful
- await does not block the JavaScript thread; other JavaScript work can continue
- Use try/catch to handle rejected Promises, including network failures and errors you throw for HTTP responses
- Add a finally block for cleanup code that runs whether the operation succeeds or fails
- Async/await is built on top of Promises - they are not competing concepts
- Use Promise.all() for independent concurrent operations when all results are required
- Use Promise.allSettled() when independent operations can succeed or fail and every result should be inspected
- await can be used inside an async function or at the top level of an ES module
Review Promises first, then practise the same async error-handling ideas with these exercises.
Promises tutorial Promise exercise 1 Promise exercise 2 Promise exercise 3What's next? Now that you know how async and await work, let's use them with real HTTP requests in the next tutorial.
- What does the async keyword do to a function?
- What does await do, and where can you use it?
- What is the difference between async/await and Promises?
- What happens if you use await outside an async function?
- How do you handle errors in async/await?
- What is the difference between using try/catch vs .catch() with async functions?
- How would you run two API calls at the same time using async/await?
- Does await block the main thread?
- Can you use async/await with forEach? What is the problem if you do?
- What does an async function return if you return a plain value like a string or a number?
Videos for this topic will be added soon.