| |

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:

StateMeaning
PendingNeither fulfilled nor rejected
FulfilledCompleted with a value
RejectedFailed 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.

CombinatorSettles whenRejects when
Promise.allAll fulfillAny rejects
Promise.allSettledAll settleNever
Promise.raceFirst settlesFirst settles with rejection
Promise.anyFirst fulfillsAll 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

StateMeaning
PendingNot yet settled
FulfilledCompleted with value
RejectedFailed with reason

Methods

MethodPurpose
.then(fn)Attach fulfillment handler
.catch(fn)Attach rejection handler
.finally(fn)Run regardless of outcome
async functionDeclare an async function
await promiseSuspend until the promise settles

Combinators

CombinatorSettles whenRejects when
Promise.allAll fulfillAny rejects
Promise.allSettledAll settleNever
Promise.raceFirst settlesFirst settles with rejection
Promise.anyFirst fulfillsAll reject

Error Handling

PatternUse case
try/catchAround await
.catch()End of a promise chain
util.promisifyWrap callback APIs
Process handlerLast resort for unhandled rejections

Concurrency Patterns

PatternPurpose
Sequential awaitWhen steps depend on each other
Promise.allWhen all results are needed
Promise.allSettledWhen partial failure is acceptable
Promise.raceWhen the fastest result wins
Promise.anyWhen 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

PitfallWhy It HappensFix
Missing awaitForgot the keywordAdd await
Unhandled rejectionNo .catch() or try/catchHandle the rejection
Slow sequential awaitsAwaiting in a loopUse Promise.all
Promise.all fails fastOne rejection rejects allUse allSettled if partial results matter
Return value is a promiseForgot await on callAwait or .then()
Async function in forEachforEach ignores the returnUse 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

ItemValue
PromisePlaceholder for an eventual value
StatesPending, fulfilled, rejected
.then()Fulfillment handler
.catch()Rejection handler
.finally()Runs regardless
asyncDeclares an async function
awaitSuspends until the promise settles
Error handlingtry/catch or .catch()
Promise.allConcurrent, fails fast
Promise.allSettledConcurrent, waits for all
Promise.raceFirst to settle
Promise.anyFirst to fulfill
util.promisifyWrap 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.
  • async marks 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.
  • await suspends 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/catch handles rejections in async/await. A rejection from an awaited promise is thrown at the await line. The catch block receives the reason.
  • Promise.all runs 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.allSettled waits for all results. Use it when partial failure is acceptable and the successful results should still be used.
  • Promise.race and Promise.any settle with the first result. race settles with the first to settle, including a rejection. any waits 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 the await, use Promise.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!