Docs

15.4-Error-Handling-Async

9.4 Error Handling in Async Code

Overview

Proper error handling in asynchronous code is crucial for building robust applications. Unlike synchronous code where errors propagate immediately through the call stack, async errors require special handling techniques to prevent silent failures and ensure graceful recovery.


Error Propagation in Async Code

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 SYNCHRONOUS ERROR FLOW                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚    function a() {                                               β”‚
β”‚        throw new Error()  ──┐                                   β”‚
β”‚    }                        β”‚                                   β”‚
β”‚                             β–Ό                                   β”‚
β”‚    function b() {    ───► catch here OR propagate up            β”‚
β”‚        a()                                                      β”‚
β”‚    }                                                            β”‚
β”‚                             β”‚                                   β”‚
β”‚    function c() {    β—„β”€β”€β”€β”€β”€β”˜                                    β”‚
β”‚        try {                                                    β”‚
β”‚            b()        ───► catch here                           β”‚
β”‚        } catch(e) {}                                            β”‚
β”‚    }                                                            β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 ASYNCHRONOUS ERROR FLOW                         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚    Promise                                                      β”‚
β”‚       β”‚                                                         β”‚
β”‚       β–Ό                                                         β”‚
β”‚    .then() ──► return/throw ──┐                                 β”‚
β”‚                               β”‚                                 β”‚
β”‚       β–Ό                       β–Ό                                 β”‚
β”‚    .then() ◄── success    .catch() ◄── error                    β”‚
β”‚       β”‚                       β”‚                                 β”‚
β”‚       β–Ό                       β–Ό                                 β”‚
β”‚    .finally() β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                 β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Callback Error Handling

Error-First Convention

// The callback receives (error, result)
function fetchData(callback) {
  setTimeout(() => {
    const error = Math.random() > 0.5 ? new Error('Failed') : null;
    const data = error ? null : { value: 42 };
    callback(error, data);
  }, 100);
}

// Always check error first
fetchData((err, data) => {
  if (err) {
    console.error('Error:', err.message);
    return;
  }
  console.log('Data:', data);
});

Nested Callback Error Handling

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         CALLBACK ERROR HANDLING PATTERN                       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                              β”‚
β”‚  step1((err1, result1) => {                                  β”‚
β”‚      β”‚                                                       β”‚
β”‚      β”œβ”€β”€ if (err1) return handleError(err1);  ◄─── Check 1   β”‚
β”‚      β”‚                                                       β”‚
β”‚      └── step2(result1, (err2, result2) => {                 β”‚
β”‚              β”‚                                               β”‚
β”‚              β”œβ”€β”€ if (err2) return handleError(err2); ◄── 2   β”‚
β”‚              β”‚                                               β”‚
β”‚              └── step3(result2, (err3, result3) => {         β”‚
β”‚                      β”‚                                       β”‚
β”‚                      └── if (err3) return handleError(err3); β”‚
β”‚                      β”‚                                       β”‚
β”‚                      └── success(result3);                   β”‚
β”‚                  });                                         β”‚
β”‚          });                                                 β”‚
β”‚  });                                                         β”‚
β”‚                                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Promise Error Handling

Basic .catch()

fetch('/api/data')
  .then((response) => response.json())
  .then((data) => processData(data))
  .catch((error) => {
    // Catches errors from any previous .then()
    console.error('Pipeline failed:', error);
  });

Error Recovery

fetch('/api/primary')
  .catch((err) => {
    console.log('Primary failed, trying backup...');
    return fetch('/api/backup'); // Return fallback promise
  })
  .then((response) => response.json())
  .catch((err) => {
    console.log('All sources failed:', err);
    return { default: true }; // Return fallback value
  });

Error Transformation

class APIError extends Error {
  constructor(message, status, response) {
    super(message);
    this.status = status;
    this.response = response;
  }
}

fetch('/api/data')
  .then((response) => {
    if (!response.ok) {
      throw new APIError(`HTTP ${response.status}`, response.status, response);
    }
    return response.json();
  })
  .catch((error) => {
    if (error instanceof APIError) {
      // Handle API-specific errors
      switch (error.status) {
        case 404:
          return null;
        case 401:
          redirectToLogin();
          break;
        default:
          throw error; // Re-throw unknown
      }
    }
    throw error; // Re-throw non-API errors
  });

Async/Await Error Handling

Basic try/catch

async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Failed to fetch user:', error);
    return null;
  }
}

Multiple try/catch Blocks

async function processOrder(orderId) {
  let order;

  // Step 1: Fetch order
  try {
    order = await fetchOrder(orderId);
  } catch (error) {
    throw new Error(`Failed to fetch order: ${error.message}`);
  }

  // Step 2: Validate order
  try {
    await validateOrder(order);
  } catch (error) {
    throw new Error(`Order validation failed: ${error.message}`);
  }

  // Step 3: Process payment
  try {
    await processPayment(order);
  } catch (error) {
    // Payment failed - might need to rollback
    await rollbackOrder(order);
    throw new Error(`Payment failed: ${error.message}`);
  }

  return { success: true, orderId };
}

Error Handling Patterns

Pattern 1: Error Tuple (Go-style)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              ERROR TUPLE PATTERN                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                            β”‚
β”‚  async function safeAwait(promise) {                       β”‚
β”‚      try {                                                 β”‚
β”‚          const result = await promise;                     β”‚
β”‚          return [null, result];    ◄── [error, result]     β”‚
β”‚      } catch (error) {                                     β”‚
β”‚          return [error, null];                             β”‚
β”‚      }                                                     β”‚
β”‚  }                                                         β”‚
β”‚                                                            β”‚
β”‚  // Usage:                                                 β”‚
β”‚  const [err, data] = await safeAwait(fetchData());         β”‚
β”‚  if (err) {                                                β”‚
β”‚      // Handle error                                       β”‚
β”‚  }                                                         β”‚
β”‚  // Use data                                               β”‚
β”‚                                                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Pattern 2: Result Object

async function fetchWithResult(url) {
  try {
    const response = await fetch(url);
    const data = await response.json();
    return { success: true, data, error: null };
  } catch (error) {
    return { success: false, data: null, error };
  }
}

const result = await fetchWithResult('/api/data');
if (result.success) {
  console.log(result.data);
} else {
  console.error(result.error);
}

Pattern 3: Error Boundary Class

class AsyncErrorBoundary {
  constructor(options = {}) {
    this.onError = options.onError || console.error;
    this.fallback = options.fallback;
  }

  async run(asyncFn) {
    try {
      return await asyncFn();
    } catch (error) {
      this.onError(error);
      if (this.fallback !== undefined) {
        return typeof this.fallback === 'function'
          ? this.fallback(error)
          : this.fallback;
      }
      throw error;
    }
  }
}

const boundary = new AsyncErrorBoundary({
  onError: (err) => logToService(err),
  fallback: [],
});

const data = await boundary.run(() => fetchData());

Handling Multiple Async Errors

Promise.all Error Behavior

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            Promise.all() - FAIL FAST                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                            β”‚
β”‚  Promise.all([p1, p2, p3])                                 β”‚
β”‚       β”‚                                                    β”‚
β”‚       β”œβ”€β”€ p1 βœ“ (100ms)                                     β”‚
β”‚       β”œβ”€β”€ p2 βœ— (50ms)  ────► REJECTS IMMEDIATELY           β”‚
β”‚       └── p3 ? (never awaited)                             β”‚
β”‚                                                            β”‚
β”‚  Result: Rejects with p2's error                           β”‚
β”‚  Note: Other promises still run, results discarded         β”‚
β”‚                                                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Promise.allSettled() - WAIT FOR ALL                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                            β”‚
β”‚  Promise.allSettled([p1, p2, p3])                          β”‚
β”‚       β”‚                                                    β”‚
β”‚       β”œβ”€β”€ p1 βœ“ β†’ { status: 'fulfilled', value: ... }       β”‚
β”‚       β”œβ”€β”€ p2 βœ— β†’ { status: 'rejected', reason: ... }       β”‚
β”‚       └── p3 βœ“ β†’ { status: 'fulfilled', value: ... }       β”‚
β”‚                                                            β”‚
β”‚  Result: Always resolves with array of outcomes            β”‚
β”‚                                                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Processing allSettled Results

const urls = ['/api/users', '/api/posts', '/api/bad'];

const results = await Promise.allSettled(
  urls.map((url) => fetch(url).then((r) => r.json()))
);

const successful = results
  .filter((r) => r.status === 'fulfilled')
  .map((r) => r.value);

const failed = results
  .filter((r) => r.status === 'rejected')
  .map((r) => r.reason);

console.log(`${successful.length} succeeded, ${failed.length} failed`);

Unhandled Promise Rejections

Detection

// Node.js
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  // Log to error tracking service
});

// Browser
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled Rejection:', event.reason);
  event.preventDefault(); // Prevent default logging
});

Common Causes

CauseExampleSolution
Missing .catch()promise.then(...)Add .catch(handler)
Missing try/catchawait promiseWrap in try/catch
Fire-and-forgetdoAsync()Add handler or use .catch()
Conditional catchif (x) await pAlways handle promise

Error Aggregation

Custom AggregateError

class AsyncAggregateError extends Error {
  constructor(errors, message = 'Multiple errors occurred') {
    super(message);
    this.errors = errors;
  }

  toString() {
    const details = this.errors
      .map((e, i) => `  ${i + 1}. ${e.message}`)
      .join('\n');
    return `${this.message}:\n${details}`;
  }
}

async function runAll(asyncFns) {
  const errors = [];
  const results = [];

  for (const fn of asyncFns) {
    try {
      results.push(await fn());
    } catch (error) {
      errors.push(error);
    }
  }

  if (errors.length > 0) {
    throw new AsyncAggregateError(errors);
  }

  return results;
}

Cleanup with finally

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              CLEANUP PATTERN                                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                            β”‚
β”‚  async function withResource() {                           β”‚
β”‚      const resource = await acquire();                     β”‚
β”‚                                                            β”‚
β”‚      try {                                                 β”‚
β”‚          return await useResource(resource);               β”‚
β”‚                   β”‚                                        β”‚
β”‚                   β”œβ”€β”€β–Ί Success: returns result             β”‚
β”‚                   └──► Error: throws                       β”‚
β”‚                                                            β”‚
β”‚      } finally {                                           β”‚
β”‚          await release(resource);  ◄── ALWAYS RUNS         β”‚
β”‚      }                                                     β”‚
β”‚  }                                                         β”‚
β”‚                                                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Resource Manager Pattern

class DatabaseConnection {
  static async use(asyncFn) {
    const connection = await this.connect();
    try {
      return await asyncFn(connection);
    } finally {
      await connection.close();
    }
  }

  static async connect() {
    console.log('Connecting...');
    return { query: async (sql) => ({ rows: [] }), close: async () => {} };
  }
}

// Usage - connection always closed
await DatabaseConnection.use(async (db) => {
  return await db.query('SELECT * FROM users');
});

Error Classification

// Custom error types
class NetworkError extends Error {
  constructor(message, url) {
    super(message);
    this.name = 'NetworkError';
    this.url = url;
    this.retryable = true;
  }
}

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.retryable = false;
  }
}

class AuthError extends Error {
  constructor(message) {
    super(message);
    this.name = 'AuthError';
    this.retryable = false;
  }
}

// Centralized error handler
async function handleError(error) {
  if (error instanceof NetworkError) {
    if (error.retryable) {
      return retry(error.url);
    }
  } else if (error instanceof ValidationError) {
    showFieldError(error.field, error.message);
  } else if (error instanceof AuthError) {
    redirectToLogin();
  } else {
    // Unknown error
    logToService(error);
    throw error;
  }
}

Best Practices Summary

PracticeDescription
Always handle rejectionsEvery promise should have error handling
Fail fast when appropriateUse Promise.all for dependent operations
Use allSettled for independent opsDon't let one failure block others
Classify errorsUse custom error types for better handling
Clean up resourcesUse finally to ensure cleanup
Log errors centrallyUse global handlers as safety net
Transform errorsAdd context as errors propagate
Avoid swallowing errorsDon't catch without handling or re-throwing

Anti-Patterns to Avoid

// ❌ Empty catch - swallows error silently
try {
  await riskyOperation();
} catch (e) {} // BAD!

// ❌ Logging but not handling
try {
  await riskyOperation();
} catch (e) {
  console.log(e); // Then what?
}

// ❌ Catching then immediately throwing
try {
  await riskyOperation();
} catch (e) {
  throw e; // Pointless
}

// βœ… Better patterns
try {
  await riskyOperation();
} catch (e) {
  // Add context
  throw new Error(`Operation failed: ${e.message}`);
}

try {
  await riskyOperation();
} catch (e) {
  // Log AND handle
  logger.error(e);
  return fallbackValue;
}

Key Takeaways

  1. β€’Async errors don't propagate automatically - must be caught explicitly
  2. β€’Use try/catch with async/await - familiar synchronous-style handling
  3. β€’Use .catch() with promises - for chain-style error handling
  4. β€’Promise.allSettled for graceful degradation - when partial success is acceptable
  5. β€’Always clean up resources - use finally or try/finally patterns
  6. β€’Classify and transform errors - makes handling more precise
  7. β€’Global handlers are safety nets - not replacements for proper handling
.4 Error Handling Async - JavaScript Tutorial | DeepML