Node.js 12 🟢 Asynchronous Control Flow with Promises and Async/Await
Promises and async/await are the modern way to write asynchronous code in Node.js. A promise represents the eventual result of an asynchronous operation: it is either pending, fulfilled with a value, or rejected with an error. The await keyword pauses an async function until the promise settles, and the value is returned or the error is thrown. This makes asynchronous code read like synchronous code without blocking the thread. The event loop continues processing other work while the async function is suspended, and the function resumes when the promise resolves.
The shift from callbacks to promises was not cosmetic. Callbacks nest, and deep nesting produces the “callback hell” that made asynchronous code hard to read and error handling hard to centralize. Promises flatten the nesting and give error handling a single channel: .catch() or try/catch around an await. The async keyword marks a function as returning a promise, and await can only be used inside an async function (or at the top level of an ES module). The result is code that looks sequential but runs concurrently where the awaits overlap.
This chapter covers promise states and creation, .then() and .catch(), async/await syntax, error handling, concurrent execution with Promise.all, Promise.allSettled, Promise.race, and Promise.any, and the patterns that avoid the common mistakes.
Key point: A promise is a placeholder for a value that will be available later. await pauses an async function until the promise settles, without blocking the thread. Promise.all runs operations concurrently and fails fast; Promise.allSettled waits for all results regardless of failure; Promise.race settles with the first result; Promise.any settles with the first fulfilled result. Always handle rejections, whether with try/catch or .catch().
Why promises and async/await exist
The callback nesting problem. Sequential asynchronous operations written with callbacks nest deeper with each step. A read, then a parse, then a write, then a log becomes four levels of indentation, and error handling is repeated at each level. The code becomes hard to read and hard to modify.
The error-handling problem. Callbacks use an error-first convention, and every callback must check the error. A single missed check means an error is silently swallowed or thrown in the wrong place. Promises give error handling a single channel: any rejection propagates to the nearest .catch() or try/catch.
The composition problem. Callbacks are hard to compose. Running three operations in parallel and combining the results requires a counter or a library. Promises have built-in combinators: Promise.all, Promise.allSettled, Promise.race, and Promise.any.
The readability problem. async/await makes asynchronous code look synchronous. The await marks the point where the function suspends, and the rest of the function reads like a sequence of steps. This matches how humans reason about sequential operations.
The concurrency problem. A common mistake with await is waiting for operations sequentially when they could run concurrently. Understanding when await suspends and when to use Promise.all is what separates correct async code from slow async code.
a. Promise states
A promise has three states:
| State | Meaning |
|---|---|
| Pending | Neither fulfilled nor rejected |
| Fulfilled | Completed with a value |
| Rejected | Failed with a reason (error) |
Once a promise settles (fulfilled or rejected), it cannot change state. The result is permanent, and any .then() or .catch() attached afterward receives the settled value.
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 1000);
});
promise.then((value) => console.log(value)); // "done" after 1 second
The executor function receives two callbacks: resolve for fulfillment and reject for rejection. Calling resolve(value) fulfills the promise; calling reject(error) rejects it.
A promise that rejects without a handler produces an unhandledRejection warning. In recent Node.js versions, the default behavior for an unhandled rejection is to terminate the process, which is a deliberate design choice to surface silent errors.
b. then, catch, and finally
The .then() method attaches a fulfillment handler, and .catch() attaches a rejection handler. Both return new promises, so they can be chained.
fetchData()
.then((data) => process(data))
.then((result) => console.log(result))
.catch((err) => console.error(err))
.finally(() => cleanup());
The .then() handler receives the fulfilled value and returns a value or a promise. If it returns a value, the next .then() receives it. If it returns a promise, the chain waits for that promise.
The .catch() handler receives the rejection reason. It catches rejections from any earlier step in the chain. A .catch() placed at the end of a chain handles errors from all preceding steps.
The .finally() handler runs regardless of fulfillment or rejection. It is used for cleanup that must happen in both cases.
openConnection()
.then((conn) => query(conn))
.then((result) => process(result))
.catch((err) => console.error('query failed:', err))
.finally(() => closeConnection());
A .catch() can also recover from an error by returning a fallback value:
fetchData()
.catch((err) => {
console.error('using default:', err);
return defaultValue;
})
.then((data) => console.log(data));
The returned value becomes the fulfillment of the chain, so the subsequent .then() receives it.
c. async and await
The async keyword marks a function as asynchronous. An async function always returns a promise, and the value it returns becomes the fulfillment value.
async function fetchData() {
return 'data';
}
fetchData().then((value) => console.log(value)); // "data"
The await keyword pauses the async function until the promise settles. The value of the awaited promise becomes the result of the await expression. If the promise rejects, the await throws the rejection reason.
async function main() {
const data = await fetchData();
console.log(data);
}
main();
The await does not block the thread. The async function is suspended, and the event loop continues processing other work. When the promise settles, the function resumes with the result.
await can only be used inside an async function or at the top level of an ES module. Using it in a regular function is a syntax error.
// Top-level await in an ES module
const data = await fetchData();
console.log(data);
The return value of an async function is always a promise, even if the function returns a plain value. Awaiting an async function’s result gives the plain value.
d. Error handling
Errors in async/await are handled with try/catch. A rejection from an awaited promise is thrown at the await line, and the catch block receives it.
async function main() {
try {
const data = await fetchData();
console.log(data);
} catch (err) {
console.error('failed:', err);
}
}
The try/catch catches rejections from any awaited promise in the block, which centralizes error handling in the same way that .catch() does in a promise chain.
A rejected promise that is not awaited propagates as an unhandled rejection. This happens when an async function is called without await or .catch():
async function risky() {
throw new Error('boom');
}
risky(); // Unhandled rejection
The fix is to await the call, attach a .catch(), or handle it at a higher level:
try {
await risky();
} catch (err) {
console.error(err);
}
// Or
risky().catch((err) => console.error(err));
A common pattern is to let errors propagate up the async call stack and handle them at the boundary where the request is processed:
app.get('/users', async (req, res) => {
try {
const users = await getUsers();
res.json(users);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
The route handler is the boundary. Errors from deeper in the call stack reach this catch and are converted to an HTTP response.
e. Concurrent execution with Promise combinators
A common mistake is awaiting operations sequentially when they could run concurrently. Awaiting in a loop waits for each operation before starting the next:
// Sequential: total time is the sum of each operation
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();
If the operations are independent, they can run concurrently with Promise.all:
// Concurrent: total time is the slowest operation
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
Promise.all takes an array of promises and returns a promise that fulfills with an array of results when all fulfill. If any promise rejects, Promise.all rejects immediately with that reason, and the other results are discarded.
Promise.allSettled waits for all promises regardless of fulfillment or rejection, and returns an array of result objects:
const results = await Promise.allSettled([fetchA(), fetchB()]);
for (const result of results) {
if (result.status === 'fulfilled') {
console.log('value:', result.value);
} else {
console.log('reason:', result.reason);
}
}
Promise.race settles with the first promise to settle, whether fulfilled or rejected:
const fastest = await Promise.race([fetchA(), fetchB()]);
Promise.any settles with the first fulfilled promise and ignores rejections until all reject:
const firstSuccess = await Promise.any([fetchA(), fetchB()]);
The difference between race and any is what they do with rejections. race settles with the first result, including a rejection. any waits for the first fulfillment and rejects only if all promises reject.
| Combinator | Settles when | Rejects when |
|---|---|---|
Promise.all | All fulfill | Any rejects |
Promise.allSettled | All settle | Never |
Promise.race | First settles | First settles with rejection |
Promise.any | First fulfills | All reject |
f. Common mistakes
Forgetting to await. An async function returns a promise. Calling it without await continues immediately, and the result is a pending promise rather than the value.
async function main() {
const data = fetchData(); // Missing await
console.log(data); // Promise { <pending> }
}
Awaiting in a loop when concurrency is possible. If the operations are independent, awaiting each one sequentially is slower than running them concurrently.
// Slow
for (const url of urls) {
const data = await fetch(url);
results.push(data);
}
// Fast
const results = await Promise.all(urls.map((url) => fetch(url)));
Not handling rejections. A rejected promise without a handler causes an unhandled rejection. In recent Node.js versions, this terminates the process.
Mixing callbacks and promises. Wrapping a callback API in a promise requires care with the executor. When a promise library like util.promisify exists, using it is safer than a manual wrapper.
const { promisify } = require('util');
const readFile = promisify(require('fs').readFile);
const data = await readFile('file.txt', 'utf8');
Sequential awaits that could be concurrent. The Promise.all pattern applies whenever the operations are independent. The exception is when a later operation depends on the result of an earlier one, in which case the sequencing is required.
Complete Example Session
// ============================================
// PART 1: PROMISE BASICS
// ============================================
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 1000);
});
promise.then((value) => console.log(value)); // "done"
// ============================================
// PART 2: THEN AND CATCH
// ============================================
fetchData()
.then((data) => process(data))
.then((result) => console.log(result))
.catch((err) => console.error(err))
.finally(() => console.log('cleanup'));
// ============================================
// PART 3: ASYNC FUNCTION
// ============================================
async function fetchData() {
return 'data';
}
fetchData().then((value) => console.log(value));
// ============================================
// PART 4: AWAIT
// ============================================
async function main() {
const data = await fetchData();
console.log(data);
}
main();
// ============================================
// PART 5: TRY/CATCH
// ============================================
async function main() {
try {
const data = await fetchData();
console.log(data);
} catch (err) {
console.error('failed:', err);
}
}
// ============================================
// PART 6: SEQUENTIAL AWAIT
// ============================================
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();
// Total time: time(A) + time(B) + time(C)
// ============================================
// PART 7: CONCURRENT WITH PROMISE.ALL
// ============================================
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
// Total time: max(time(A), time(B), time(C))
// ============================================
// PART 8: PROMISE.ALLSETTLED
// ============================================
const results = await Promise.allSettled([fetchA(), fetchB()]);
for (const result of results) {
if (result.status === 'fulfilled') {
console.log('value:', result.value);
} else {
console.log('reason:', result.reason);
}
}
// ============================================
// PART 9: PROMISE.RACE AND PROMISE.ANY
// ============================================
const fastest = await Promise.race([fetchA(), fetchB()]);
const firstSuccess = await Promise.any([fetchA(), fetchB()]);
// ============================================
// PART 10: PROMISIFY CALLBACK API
// ============================================
const { promisify } = require('util');
const readFile = promisify(require('fs').readFile);
async function main() {
const data = await readFile('file.txt', 'utf8');
console.log(data);
}
These ten parts cover promise basics, .then() and .catch(), async functions, await, try/catch, sequential await, Promise.all, Promise.allSettled, Promise.race and Promise.any, and promisifying callback APIs.
Quick Reference
Promise States
| State | Meaning |
|---|---|
| Pending | Not yet settled |
| Fulfilled | Completed with value |
| Rejected | Failed with reason |
Methods
| Method | Purpose |
|---|---|
.then(fn) | Attach fulfillment handler |
.catch(fn) | Attach rejection handler |
.finally(fn) | Run regardless of outcome |
async function | Declare an async function |
await promise | Suspend until the promise settles |
Combinators
| Combinator | Settles when | Rejects when |
|---|---|---|
Promise.all | All fulfill | Any rejects |
Promise.allSettled | All settle | Never |
Promise.race | First settles | First settles with rejection |
Promise.any | First fulfills | All reject |
Error Handling
| Pattern | Use case |
|---|---|
try/catch | Around await |
.catch() | End of a promise chain |
util.promisify | Wrap callback APIs |
| Process handler | Last resort for unhandled rejections |
Concurrency Patterns
| Pattern | Purpose |
|---|---|
| Sequential await | When steps depend on each other |
Promise.all | When all results are needed |
Promise.allSettled | When partial failure is acceptable |
Promise.race | When the fastest result wins |
Promise.any | When the first success wins |
Best Practices
✅ Do This:
// Use async/await for readability
const data = await fetchData();
// Use try/catch for error handling
try {
const data = await fetchData();
} catch (err) {
console.error(err);
}
// Use Promise.all for independent operations
const [a, b] = await Promise.all([fetchA(), fetchB()]);
// Use allSettled when partial failure is acceptable
const results = await Promise.allSettled(promises);
// Use util.promisify for callback APIs
const readFile = promisify(require('fs').readFile);
❌ Don’t Do This:
// Forget to await
const data = fetchData(); // ❌ returns a promise
// Await sequentially when concurrent is possible
const a = await fetchA(); // ❌ slower than Promise.all
const b = await fetchB();
// Ignore rejections
risky(); // ❌ unhandled rejection
// Mix callbacks and promises carelessly
fs.readFile('file.txt', (err, data) => {
// ... inside an async function
}); // ❌ prefer promisified version
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Missing await | Forgot the keyword | Add await |
| Unhandled rejection | No .catch() or try/catch | Handle the rejection |
| Slow sequential awaits | Awaiting in a loop | Use Promise.all |
Promise.all fails fast | One rejection rejects all | Use allSettled if partial results matter |
| Return value is a promise | Forgot await on call | Await or .then() |
Async function in forEach | forEach ignores the return | Use for...of with await |
Real-World Examples
1. Basic Fetch
const data = await fetch(url).then((r) => r.json());
2. Try/Catch
try {
const data = await fetchData();
} catch (err) {
console.error(err);
}
3. Sequential Steps
const user = await getUser(id);
const orders = await getOrders(user.id);
4. Concurrent Fetch
const [users, products] = await Promise.all([
fetchUsers(),
fetchProducts(),
]);
5. Partial Failure
const results = await Promise.allSettled(urls.map(fetch));
const successes = results.filter((r) => r.status === 'fulfilled');
6. First Success
const fastest = await Promise.any([fetchA(), fetchB()]);
7. Timeout Pattern
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), 5000)
);
const result = await Promise.race([fetchData(), timeout]);
8. Promisify
const readFile = promisify(require('fs').readFile);
const data = await readFile('file.txt', 'utf8');
9. Loop with Await
for (const url of urls) {
const data = await fetch(url);
results.push(data);
}
10. Async Route Handler
app.get('/users', async (req, res) => {
try {
const users = await getUsers();
res.json(users);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
Visual
Promise States
┌──────────────────────────────────────────────────────────────┐
│ ┌──────────┐ │
│ │ PENDING │ │
│ └────┬─────┘ │
│ │ │
│ ├── resolve(value) ──▶ ┌───────────┐ │
│ │ │ FULFILLED │ │
│ │ └───────────┘ │
│ │ │
│ └── reject(reason) ──▶ ┌──────────┐ │
│ │ REJECTED │ │
│ └──────────┘ │
│ │
│ Once settled, the state does not change. │
└──────────────────────────────────────────────────────────────┘
Async/Await Flow
┌──────────────────────────────────────────────────────────────┐
│ async function main() { │
│ const data = await fetchData(); │
│ console.log(data); │
│ } │
│ │
│ 1. main() called; fetchData() starts │
│ 2. await suspends main; event loop continues │
│ 3. fetchData resolves │
│ 4. main resumes; data is available │
│ 5. console.log(data) runs │
│ │
│ The thread is not blocked during the await. │
└──────────────────────────────────────────────────────────────┘
Sequential vs Concurrent
┌──────────────────────────────────────────────────────────────┐
│ SEQUENTIAL: │
│ await A ────▶ await B ────▶ await C ────▶ done │
│ Total: t(A) + t(B) + t(C) │
│ │
│ CONCURRENT (Promise.all): │
│ await A ────────────┐ │
│ await B ────────────┼──▶ done │
│ await C ────────────┘ │
│ Total: max(t(A), t(B), t(C)) │
└──────────────────────────────────────────────────────────────┘
Promise Combinators
┌──────────────────────────────────────────────────────────────┐
│ Promise.all([A, B, C]) │
│ └── Fulfills with [a, b, c] when all fulfill │
│ └── Rejects immediately if any rejects │
│ │
│ Promise.allSettled([A, B, C]) │
│ └── Fulfills with status objects when all settle │
│ └── Never rejects │
│ │
│ Promise.race([A, B, C]) │
│ └── Settles with the first result (fulfill or reject) │
│ │
│ Promise.any([A, B, C]) │
│ └── Fulfills with the first fulfillment │
│ └── Rejects only if all reject │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Promise | Placeholder for an eventual value |
| States | Pending, fulfilled, rejected |
.then() | Fulfillment handler |
.catch() | Rejection handler |
.finally() | Runs regardless |
async | Declares an async function |
await | Suspends until the promise settles |
| Error handling | try/catch or .catch() |
Promise.all | Concurrent, fails fast |
Promise.allSettled | Concurrent, waits for all |
Promise.race | First to settle |
Promise.any | First to fulfill |
util.promisify | Wrap callback APIs |
Key takeaways:
- A promise represents an eventual value. It is pending, fulfilled, or rejected, and once settled, it cannot change.
.then()handles fulfillment,.catch()handles rejection, and.finally()runs regardless. asyncmarks a function as returning a promise. The value returned becomes the fulfillment value. The function always returns a promise, even when returning a plain value.awaitsuspends the async function, not the thread. The event loop continues processing other work while the function is paused. The function resumes when the promise settles.try/catchhandles rejections in async/await. A rejection from an awaited promise is thrown at theawaitline. Thecatchblock receives the reason.Promise.allruns operations concurrently. Use it when the operations are independent and all results are needed. It fails fast, rejecting as soon as any promise rejects.Promise.allSettledwaits for all results. Use it when partial failure is acceptable and the successful results should still be used.Promise.raceandPromise.anysettle with the first result.racesettles with the first to settle, including a rejection.anywaits for the first fulfillment and rejects only if all reject.- The common mistakes are missing
await, sequential awaits that could be concurrent, and unhandled rejections. Each has a clear fix: add theawait, usePromise.all, or handle the rejection.
Remember: Promises and async/await are the modern way to write asynchronous code in Node.js. A promise is a placeholder for a value that will be available later, and await pauses an async function until the promise settles without blocking the thread. The abstractions flatten the nesting of callbacks, centralize error handling, and provide combinators for concurrent execution. The key decisions are whether operations are sequential or independent and whether partial failure is acceptable. Promise.all for concurrent execution, Promise.allSettled for partial failure, Promise.race for the fastest result, and Promise.any for the first success cover the common cases. Always handle rejections, whether with try/catch or .catch(), because an unhandled rejection terminates the process in modern Node.js. Understanding these patterns is what makes asynchronous code correct, readable, and fast.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!