Key Takeaways
async/awaitbuilds on Promises; it changes how you express asynchronous control flow, not the underlying operation.- An async function always returns a Promise. Returning a plain value fulfills it; an uncaught exception rejects it.
awaitsuspends the surrounding async function, not the JavaScript thread.- Use sequential awaits for dependent operations and
Promise.all()to collect independent operations running concurrently. - Handle failures where you can recover: with an awaited
try/catch, or with.catch()on the returned Promise.
JavaScript async await lets you consume Promise-based operations using straightforward assignments, conditionals, and try/catch. It doesn't replace Promises: async functions produce them, and await gives you a way to suspend and resume execution around their results.
The benefit is clearer control flow—not inherently faster asynchronous work. Choose the syntax that makes dependencies and error handling obvious, then measure performance-sensitive paths.
JavaScript async await example
This example waits for a Promise and handles either outcome. Run it in a browser console or a JavaScript file in Node.js; later snippets marked as fragments reuse its asyncActivity helper.
const asyncActivity = (shouldFail = false) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error("Activity failed"));
} else {
resolve(1);
}
}, 5000);
});
};
async function myFunction(shouldFail = false) {
try {
const result = await asyncActivity(shouldFail);
console.log("Result is: " + result);
} catch (error) {
console.error("Error:", error);
}
}
myFunction();
myFunction(true);The first call logs Result is: 1; the second logs the error. The timer simulates asynchronous work. When the awaited Promise fulfills, result receives its fulfillment value; when it rejects, await throws the rejection reason into the surrounding try block.
If the underlying abstraction is new to you, start with our guide to JavaScript Promises or MDN's Using promises guide.
The equivalent .then() version
// Fragment: reuse asyncActivity from the opening example.
function myFunctionWithThen(shouldFail = false) {
return asyncActivity(shouldFail)
.then((result) => {
console.log("Result is: " + result);
})
.catch((error) => {
console.error("Error:", error);
});
}
myFunctionWithThen();For this workflow, both versions wait for the same result and handle failure by logging it. Returning the chain also lets callers observe when myFunctionWithThen() finishes.
What an async function returns
Every call to an async function returns a Promise, even if its body contains no await. Returning a plain value fulfills that Promise with the value; throwing an exception that isn't caught inside the function rejects it.
async function readValue() {
return 1;
}
async function fail() {
throw new Error("Example failure");
}
readValue()
.then((value) => console.log(value))
.catch((error) => console.error(error));
fail().catch((error) => console.error(error.message));This prints 1 and reports Example failure. Notice that fail() has no local try/catch: its caller handles the rejection. A missing local catch does not, by itself, create an unhandled rejection.
What await pauses—and what it doesn't
await pauses the continuation of the surrounding async function. The caller can continue, and other event-loop work can run while the function waits. Even awaiting an already-fulfilled Promise defers the code after the await.
However, async doesn't move your function to another thread. Its body starts running synchronously until the first suspension point, so CPU-heavy synchronous work can still block the thread. For the scheduling model behind this behavior, see the Node.js event loop explained.
Using Promise chains with async/await
When each operation needs the previous result, both a Promise chain and sequential awaits express the same dependency order. This complete example defines all three operations and gives the two implementations distinct names.
const async1 = () => Promise.resolve(1);
const async2 = (value) => Promise.resolve(value + 1);
const async3 = (value) => Promise.resolve(value * 2);
function runWithChain() {
return async1()
.then((result1) => async2(result1))
.then((result2) => async3(result2));
}
async function runWithAwait() {
const result1 = await async1();
const result2 = await async2(result1);
const result3 = await async3(result2);
return result3;
}
runWithChain()
.then((result) => console.log("Chain:", result))
.catch((error) => console.error("Chain failed:", error));
runWithAwait()
.then((result) => console.log("Await:", result))
.catch((error) => console.error("Async function failed:", error));Both implementations produce 4. The helpers use immediately fulfilled Promises to keep the example focused on control flow rather than network or filesystem setup.
Promise chain: async1() --then--> async2(result1) --then--> async3(result2)
Sequential: async1() --await-> async2(result1) --await-> async3(result2)
Dependency: result1 ---------> result2 --------------> result3In the chain, returning each operation's Promise makes the next handler wait for its result. In the async function, await makes that dependency explicit in an assignment. These are equivalent dependency flows, not a claim that every observable detail of async functions is identical to a hand-written chain. See our Promise chaining tutorial for more on returning values between handlers.
Async await vs Promises
These aren't competing asynchronous systems. You can await a Promise-returning API, attach .then() to an async function's result, and use Promise composition methods inside an async function.
| Concern | Explicit Promise methods | Async/await |
|---|---|---|
| Return value | .then() returns a new Promise. | An async function always returns a Promise. |
| Reading a result | A fulfillment handler receives the value. | await evaluates to the fulfillment value. |
| Error propagation | A thrown error in a handler rejects the returned Promise. | An uncaught exception rejects the async function's Promise. |
| Dependent operations | Return each operation from the previous handler. | Await each result before starting its dependent operation. |
| Concurrent operations | Compose their Promises with methods such as Promise.all(). | Await the same composition methods. |
The behavior behind this comparison is documented in the Promise.then() reference and the async-function and await references below.
Prefer async/await when a workflow has several steps, branches, or local recovery paths. A short transformation can still be clearer as a chain. Neither choice guarantees lower overhead; benchmark the workload rather than treating syntax as a performance optimization.
Sequential await vs concurrent Promise.all()
The previous three-stage example must be sequential because its operations depend on one another. Independent operations don't have that constraint. Here are sequential and concurrent versions of the same two independent reads:
const delay = (value, milliseconds) =>
new Promise((resolve) => {
setTimeout(() => resolve(value), milliseconds);
});
const getUser = () => delay({ id: 1 }, 300);
const getSettings = () => delay({ theme: "dark" }, 200);
async function loadSequentially() {
const user = await getUser();
const settings = await getSettings();
return { user, settings };
}
async function loadConcurrently() {
const [user, settings] = await Promise.all([
getUser(),
getSettings(),
]);
return { user, settings };
}
loadConcurrently()
.then((result) => console.log(result))
.catch((error) => console.error("Loading failed:", error));loadSequentially() starts the settings read only after the user read finishes. loadConcurrently() calls both helpers before awaiting their combined result. The calls start the operations; Promise.all() collects their results, preserving input order regardless of completion order.
Promise.all() rejects when an input rejects, but it doesn't cancel the remaining operations. Use Promise.allSettled() when you need an outcome for every operation, including failures.
An await inside a loop isn't automatically wrong: it's appropriate when each iteration depends on the previous result or when you deliberately need ordered execution. For large independent workloads, use bounded batches or a concurrency limit rather than starting every operation at once.
Async await arrow function syntax
Put async before the arrow function's parameter list. Use parentheses for zero or multiple parameters; a single simple parameter can omit them.
// Fragment: reuse asyncActivity from the opening example.
const myArrowFunction = async () => {
try {
const result = await asyncActivity();
console.log("Result is: " + result);
} catch (error) {
console.error("Error:", error);
}
};
myArrowFunction();await must be inside an async function or at module top level. A nested ordinary callback doesn't become async just because its enclosing function is async. Our JavaScript functions tutorial covers the underlying function syntax.
Promises and arrow functions arrived in ES2015, also called ES6. Async functions, including async arrows, were standardized later in ES2017; calling async functions an ES6 feature is incorrect.
Async await error handling with try/catch
A try/catch handles exceptions thrown while executing its try block, including a rejection reason thrown by an await there. It doesn't automatically handle every rejected Promise created inside the block.
This example preserves an intentional exception after a successful await:
// Fragment: reuse asyncActivity from the opening example.
async function errorProneFunction() {
try {
const result = await asyncActivity();
console.log(result);
throw new Error("Intentional error");
} catch (error) {
console.error("Caught an error:", error.message);
}
}
errorProneFunction();The catch handles either a rejection from asyncActivity() or the explicit Intentional error. If you instead call asyncActivity() without awaiting or returning its Promise, a later rejection won't reach that local catch.
Returning a Promise passes its eventual failure to the caller. If a local catch must handle that failure, use return await promise inside its try block rather than return promise.
Unhandled rejections depend on the host
A rejection isn't unhandled merely because the async function lacks a local try/catch. The caller can observe it with .catch() or another awaited try/catch; attach handling promptly rather than leaving a rejected Promise unobserved.
Browsers expose an unhandledrejection event. In Node.js, an unhandled rejection can terminate the process, depending on configuration and installed handlers. Treat global reporting as a diagnostic safety net, not your normal recovery strategy.
Using await at module top level
Top-level await is available in JavaScript modules, not ordinary classic scripts or CommonJS files. In Node.js, a .mjs extension explicitly selects the ES module format. Save this as example.mjs and run node example.mjs:
// example.mjs
try {
const result = await Promise.resolve(1);
console.log(result);
} catch (error) {
console.error("Initialization failed:", error);
}If your file must remain CommonJS, put the await inside an async function and observe the Promise returned by that function.
Common Mistakes
- Expecting array callbacks to wait.
forEach()doesn't wait for Promises returned by async callbacks. Usefor...offor ordered work orPromise.all(items.map(...))for independent work. - Starting independent Promises, then awaiting them one by one. A later Promise can reject before execution reaches its await. Compose them promptly with
Promise.all()so their rejections are observed together. - Forgetting to return a Promise from a chain handler. The next handler can't wait for an operation that the previous handler started but didn't return.
FAQ
Does an async function always return a Promise?
Yes. An async function returns a Promise even when it returns a plain value or has no await expression. An uncaught exception in its body rejects that Promise.
Does await block the JavaScript thread?
No. await suspends the surrounding async function's continuation, allowing other event-loop work to continue. Synchronous CPU-heavy code inside the function can still block the thread.
How do you handle a rejected Promise with async/await?
Await the Promise inside try/catch, or attach .catch() to the Promise returned by the async function. You don't need a catch block in every function, but a caller must arrange to handle failures.
How do you run independent async operations concurrently?
Call the independent operations before waiting for their combined result with Promise.all(). Use sequential await when one operation needs the previous result, and Promise.allSettled() when you need every success or failure outcome.
Can you use await outside an async function?
Yes, at the top level of a JavaScript module. Ordinary classic scripts and CommonJS files don't allow top-level await; put the code inside an async function or use an ES module.
Further Reading
- MDN: async function — Promise returns, execution order, and chain rewrites.
- MDN: await — fulfillment values, rejection propagation, and suspension.
- MDN: Using promises — chaining, composition, and floating Promises.
- MDN: Promise.all() — concurrent work and fail-fast behavior.
- MDN: Promise.allSettled() — collecting successful and failed outcomes.
- MDN: Arrow function expressions — async arrow syntax and parameter rules.
- Node.js: ECMAScript modules — module formats and top-level await.
- Node.js: Process — unhandled-rejection reporting and process behavior.
