Converting Promises to Async Await in JavaScript
Learn how to refactor promise chains into async/await syntax step by step. See side-by-side conversions, handle errors with try...catch, and avoid common conversion pitfalls.
Converting promises to async/await means rewriting .then() callbacks as sequential await statements inside an async function. The runtime behavior stays the same. The code becomes linear: you read it top to bottom instead of following a chain of callbacks.
Every promise chain can be converted, and every async/await function can be converted back to promises. They are two syntaxes for the same underlying mechanism.
// Promise chain
function getUserEmail(id) {
return fetch(`/users/${id}`)
.then((res) => res.json())
.then((user) => user.email);
}The async/await version below does the same fetch and the same two-step unwrap, but each intermediate value gets its own line and its own name instead of living inside a callback parameter.
// async/await equivalent
async function getUserEmail(id) {
const res = await fetch(`/users/${id}`);
const user = await res.json();
return user.email;
}Both functions return a promise that resolves to the user's email. The second version reads like synchronous code, with each step on its own line and the intermediate res and user values available as named variables.
The mechanical conversion rules
Converting a promise chain to async/await follows a small set of rules that apply the same way every time, no matter how the chain is shaped.
| Promise pattern | async/await equivalent |
|---|---|
| then callback | const result = await promise, then the body |
| catch callback | try block, then catch (err) |
| finally callback | try block, then finally |
| Returning a value from then | Return the value directly |
| Returning a promise from then | return await promise, or just return promise |
The most important rule is that whatever was inside a then callback body moves directly after the matching await line, and the callback's parameter name becomes a plain variable.
// Before: promise chain
getUser(1)
.then((user) => {
console.log(`Processing ${user.name}`);
return getPosts(user.id);
})
.then((posts) => {
console.log(`Found ${posts.length} posts`);
return posts.filter((p) => p.published);
});The converted version below keeps the exact same two log statements and the same filter call, just written as sequential steps instead of chained callbacks.
// After: async/await
async function processUser() {
const user = await getUser(1);
console.log(`Processing ${user.name}`);
const posts = await getPosts(user.id);
console.log(`Found ${posts.length} posts`);
return posts.filter((p) => p.published);
}Notice that the console.log calls stay exactly where they were. The only thing that changes is how values move between steps, from chain parameters to ordinary variables.
Converting error handling
Promise chains use .catch() at different positions for different error-handling strategies. Each catch becomes a try...catch at the same logical position in the async version.
// Before: chain with mid-chain error recovery
fetchUser(id)
.then((user) => processUser(user))
.catch((err) => {
console.warn("Processing failed, using fallback:", err.message);
return fallbackUser;
})
.then((user) => saveUser(user));The try block below wraps exactly the two steps that the catch covered in the chain above, and the catch block provides the same fallback value.
// After: async/await with equivalent try/catch
async function handleUser(id) {
let user;
try {
user = await fetchUser(id);
user = await processUser(user);
} catch (err) {
console.warn("Processing failed, using fallback:", err.message);
user = fallbackUser;
}
return saveUser(user);
}Execution continues with saveUser regardless of which path the try...catch took, matching how the original chain always reached its final then call.
A final catch at the end of a chain becomes a try...catch around the whole async function body, or you can let the caller handle it since async functions already return promises.
// Before: error handling at the call site
fetchData().then(render).catch(showError);
// After: still handle at the call site
async function main() {
try {
const data = await fetchData();
render(data);
} catch (err) {
showError(err);
}
}Handling Promise.all and other combinators
Promise.all and similar combinators already return promises, so they slot into an async function without any restructuring. You still call them the same way, only now with await in front instead of a then.
// Before: Promise.all in a chain
function loadPage() {
return Promise.all([fetchUser(1), fetchPosts(1), fetchComments(1)])
.then(([user, posts, comments]) => {
return { user, posts, comments };
});
}The array destructuring that used to live inside the then callback parameter moves to the left side of the await instead.
// After: destructure the awaited result
async function loadPage() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1),
]);
return { user, posts, comments };
}The parallel execution is preserved exactly. Promise.all still starts all three fetches at once, and await simply waits for all of them together. Converting to await here does not turn the parallel calls into sequential ones.
The array literal passed to Promise.all still creates every promise before any of them is awaited, so nothing about the timing changes just because the syntax looks more linear. For more on parallel patterns, see JavaScript Async Await Guide: Complete Tutorial.
Handling Promise.race and Promise.any
Promise combinators like race and any need no conversion at all, because they already return a promise on their own. Wrapping the call in an async function and awaiting or returning it works exactly the same as before.
function fetchWithTimeout(url, ms) {
return Promise.race([
fetch(url),
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), ms)),
]);
}This function works identically whether the surrounding code calls it with .then() or awaits it inside another async function, since fetchWithTimeout itself never needed to change.
The lesson here is not to hunt for a conversion step that does not exist. If a function already returns a promise directly, without a .then() chain wrapped around it, there is nothing left to rewrite. For more on this pattern, see How to Use Promise Race in JS: Complete Guide.
Common conversion mistakes
Forgetting to make the function async. Using await inside a function that is not marked async throws a syntax error, since await is only meaningful inside an async function.
// Mistake: missing async
function getData() {
const res = await fetch("/data"); // SyntaxError
}
// Correct
async function getData() {
const res = await fetch("/data");
}Leaving then callbacks half-converted. A common halfway state mixes await at the top of a function with a leftover then further down. Either finish the conversion or leave the original chain intact.
// Confusing half-conversion
async function loadStuff() {
const user = await fetchUser(1);
return getPosts(user.id).then((posts) => posts.length);
}
// Clean: finish the conversion
async function loadStuff() {
const user = await fetchUser(1);
const posts = await getPosts(user.id);
return posts.length;
}Shadowing variables between chain steps. In a promise chain, each then callback creates its own scope. In async/await, every variable shares the function's scope, so reusing the same name for two different steps causes the second one to overwrite the first.
// Promise chain: each then has its own scoped result
fetchA()
.then((result) => transformA(result))
.then((result) => transformB(result)); // Different resultThe async/await version needs distinct names for each step since there is no separate scope to keep them apart automatically.
// async/await: use distinct names
async function process() {
const resultA = await fetchA();
const transformedA = transformA(resultA);
const resultB = await fetchB(transformedA);
return transformB(resultB);
}Using await inside a non-async callback. Await only works directly inside async functions. It does not work inside callbacks passed to .map(), .forEach(), or event handlers unless those callbacks are marked async too.
// Mistake: await inside .map() callback (not async)
const slugs = urls.map((url) => {
const res = await fetch(url); // SyntaxError
});The fix marks the callback itself as async, then wraps the whole array of promises in Promise.all so the outer function can await them together.
// Correct: make the callback async, then use Promise.all
const slugs = await Promise.all(
urls.map(async (url) => {
const res = await fetch(url);
return res.url;
})
);When to convert and when to keep promises
| Convert to async/await when | Keep promise chains when |
|---|---|
| The chain has 3 or more steps | The chain is one or two then calls |
| You need to name intermediate values | Values flow naturally without names |
| Error handling needs try...catch | A single catch at the end is enough |
| You have conditional logic between steps | Each step is a single expression |
| The chain is hard to read | The chain is already clear |
Neither style is better in absolute terms. Choose the one that makes the intent clearest. A short two-step chain is often cleaner left as-is than wrapped in an async function with two await lines, but the moment a step needs a conditional, async/await becomes the clearer choice.
For the promise chaining fundamentals, see JavaScript Promise Chaining: A Complete Guide.
Rune AI
Key Insights
- Every .then(callback) becomes const result = await promise; followed by the callback body.
- Replace .catch() with try...catch blocks at the same position in the flow.
- async/await makes intermediate values available as named variables instead of chain parameters.
- Promise.all() works naturally with await: just await the result.
- Avoid mixing .then() and await in the same function without a clear reason.
Frequently Asked Questions
Can every promise chain be converted to async/await?
Should I always use async/await instead of promises?
What happens to .catch() when I convert to async/await?
Conclusion
Converting a promise chain to async/await is a mechanical process. Find each .then(), move its callback body after an await, assign the result to a variable instead of chaining, and wrap error paths in try...catch. The runtime behavior is identical. The difference is in how the code reads.
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.