JavaScript Async Await Guide: Complete Tutorial
async and await let you write asynchronous JavaScript that reads like synchronous code, with cleaner error handling and clear execution order.
async and await are JavaScript keywords that let you write asynchronous code that looks and reads like synchronous code. Under the hood, they still use promises, but the syntax hides the chaining and makes the flow of values easier to follow.
An async function always returns a promise. Inside it, the await keyword pauses execution until a promise settles, then unwraps the resolved value. You read the code top to bottom, just like synchronous code, even though it is waiting on a network request, a file read, or a timer.
async function fetchUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`);
const user = await response.json();
return user;
}
fetchUser(1).then((user) => console.log(user.name));The function pauses at each await, waits for the result, then continues. The caller gets back a promise and uses then, or another await, to get the final value.
async functions always return a promise
Marking a function async does one thing: it makes the function return a promise. Whatever you return from the function becomes the resolved value of that promise.
async function add(a, b) {
return a + b;
}
const result = add(3, 4);
console.log(result); // Promise { 7 }Even though add returns the plain number 7, the caller receives a promise that resolves to 7, not the number directly. Getting the actual value takes one more step.
add(3, 4).then((sum) => console.log(sum)); // 7
// Or inside another async function:
// const sum = await add(3, 4);This is the single most important rule of async functions. They always wrap the return value in a promise, and there is no way to return a bare value from one.
The await keyword: pause until resolved
The await keyword can only be used inside an async function. It takes a promise, pauses the function, and waits for the promise to settle. A resolved promise hands await its value, and a rejected one makes await throw.
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}This helper wraps setTimeout in a promise so it can be awaited, a pattern covered in more depth in JavaScript setTimeout Behavior: Complete Guide. Using it inside an async function pauses that function without blocking anything else.
async function greet() {
console.log("Hello");
await delay(2000);
console.log("World");
}
greet();Running greet prints the first line immediately, waits two full seconds while the rest of the page stays responsive, then prints the second line.
Hello
(2 second pause)
WorldThe await only pauses the greet function itself, not the entire program. Other code on the page keeps running during that two second wait.
How async/await compares to promise chaining
The same logic can be written as a promise chain or as an async function. The table below compares how each style handles structure, naming, and errors before looking at the code itself.
| Aspect | Promise chaining | async/await |
|---|---|---|
| Structure | Callbacks linked with then | Sequential statements with await |
| Naming | Values live only inside callbacks | Each step gets its own named variable |
| Errors | Handled with a catch at the end | Handled with try...catch around the block |
| Best for | Short chains, functional style | Longer flows with branching logic |
async function getUserPosts(userId) {
const userRes = await fetch(`/users/${userId}`);
const user = await userRes.json();
const postsRes = await fetch(`/posts?userId=${user.id}`);
const posts = await postsRes.json();
return posts.filter((p) => p.published);
}Each awaited line stores its result in a named variable, so user and posts are readable on their own instead of being buried inside nested callbacks. The equivalent promise chain for this exact function is shown in JavaScript Promise Chaining: A Complete Guide.
Both chains do the same work in the same order. The diagram lines up each promise-chain step next to its async/await equivalent, showing that the two styles are the same underlying steps written in a different shape.
Error handling with try...catch
The biggest practical benefit of async/await is that you can use a try...catch block for error handling, just like synchronous code. This is often cleaner than chaining multiple catch calls.
async function loadDashboard(userId) {
try {
const user = await fetch(`/users/${userId}`).then((r) => r.json());
const posts = await fetch(`/posts?userId=${userId}`).then((r) => r.json());
return { user, posts };
} catch (error) {
console.error("Dashboard load failed:", error.message);
return { user: null, posts: [] };
}
}Any await that rejects inside the try block jumps straight to catch. One try...catch protects the whole sequence, so you do not need to attach a separate catch to each individual fetch.
You can also handle errors at the call site instead of inside the function itself.
async function main() {
try {
const dashboard = await loadDashboard(42);
render(dashboard);
} catch (err) {
showErrorBanner("Could not load dashboard");
}
}Since async functions return promises, you can use .catch() on the returned promise instead of try/catch if that reads better for the situation. For a deeper look at structuring async error handling, see Handling Async Errors with Try Catch in JS Guide.
Sequential vs parallel execution
Await serializes promises by default. If you have three independent fetches and await each one in turn, they run one after another instead of at the same time.
// Sequential: runs one at a time (slower)
async function loadSequential() {
const user = await fetchUser(1);
const posts = await fetchPosts(1);
const comments = await fetchComments(1);
return { user, posts, comments };
}When the requests do not depend on each other, start them all at once with Promise.all and await the combined result instead.
// Parallel: all three start at once (faster)
async function loadParallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1),
]);
return { user, posts, comments };
}Promise.all starts every promise in the array immediately and resolves once all of them have resolved. If any single one rejects, the whole call rejects right away with that error.
| Pattern | Total time (3 x 200ms fetches) | When to use |
|---|---|---|
| Sequential await | ~600ms | Each step depends on the previous one |
| Promise.all | ~200ms | All fetches are independent |
| Promise.allSettled | ~200ms | You want partial results, not all-or-nothing |
Promise.allSettled runs every promise to completion regardless of individual failures, which is useful when a partial result is still valuable.
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(-1), // This will fail
fetchUser(2),
]);
results.forEach((result) => {
console.log(result.status === "fulfilled" ? result.value.name : result.reason.message);
});Unlike Promise.all, one rejected fetch here does not stop the others. Each entry in the results array reports its own status, so the loop can print a name or a failure message depending on what happened.
Common async/await mistakes
Forgetting that async functions return promises. Calling an async function and expecting the raw return value is a common beginner error.
async function getName() {
return "Taylor";
}
// Mistake: treating the result as a string
const rawName = getName();
console.log(rawName.toUpperCase()); // Error: rawName.toUpperCase is not a functionFix it by awaiting the call before using the value, since calling getName on its own hands back a pending promise, not the string itself.
// Correct: await or .then()
const name = await getName();
console.log(name.toUpperCase()); // "TAYLOR"Using await on non-promises. Await works on any value, not just promises. If the value is not a promise, it is wrapped in a resolved promise and returned right away. This is harmless but unnecessary.
Mixing then and await without reason. Both patterns work, but mixing them in the same function makes the code harder to follow. Picking one style per function keeps the logic easy to scan.
// Confusing mix
async function loadStuff() {
const user = await fetchUser(1);
fetchPosts(user.id).then((posts) => console.log(posts));
}
// Cleaner: pick one
async function loadStuff() {
const user = await fetchUser(1);
const posts = await fetchPosts(user.id);
console.log(posts);
}Awaiting inside loops sequentially when parallelism is possible. Looping over an array and awaiting each item one at a time serializes work that could run at the same time instead.
// Slow: one at a time
async function fetchAllUsers(ids) {
const users = [];
for (const id of ids) {
users.push(await fetchUser(id));
}
return users;
}The fast version starts every request immediately by mapping the array into a list of promises first, then waits for all of them together instead of waiting on each one before starting the next.
// Fast: all at once
async function fetchAllUsers(ids) {
const userPromises = ids.map((id) => fetchUser(id));
return Promise.all(userPromises);
}Swallowing errors. An empty catch block hides failures silently, which turns a debuggable error into a mystery bug that only shows up much later.
// Mistake: silent failure
try {
await riskyOperation();
} catch {
// Nothing. Bug stays hidden.
}The corrected version keeps the exact same structure but adds one log line inside the catch block, which is often enough to make a hidden failure visible again.
// Better: at least log it
try {
await riskyOperation();
} catch (error) {
console.error("riskyOperation failed:", error.message);
}When to use async/await vs promise chaining
| Use async/await when | Use promise chaining when |
|---|---|
| You need intermediate variable names | The chain is short and simple |
| Error handling with try...catch reads better | You prefer a compact, functional style |
| You have conditional logic between steps | Each step is a single expression |
| The flow is complex with branches | You are mapping over an array of async tasks |
Both patterns compile to the same promise machinery underneath. The choice comes down to readability for the task at hand, not performance.
Rune AI
Key Insights
- async functions always return a promise, even when you return a plain value.
- await pauses execution inside an async function until the promise settles.
- Use try...catch to handle errors in async functions the same way as synchronous code.
- Promises run in parallel when created before awaiting; use Promise.all to coordinate them.
- Do not mix .then() and await in the same function without a clear reason.
Frequently Asked Questions
Does an async function always return a promise?
Can I use await outside an async function?
Is async/await faster than promise chaining?
Conclusion
async and await do not replace promises. They make promises easier to read and write. An async function always returns a promise, await pauses execution until a promise settles, and try...catch handles errors the same way it handles synchronous ones. Once you are comfortable with both patterns, you can choose the one that reads more naturally for the task.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.