正在加载,请稍候…

Async/Await Error Handling: Every Pattern You Need

Master async/await error handling in JavaScript and TypeScript. Covers try-catch, Promise.allSettled, AbortController, timeout patterns, and avoiding common mistakes.

Why Async Error Handling Is Tricky

Synchronous errors are caught and propagated naturally up the call stack. Asynchronous errors — errors inside Promises or async functions — behave differently, and the gaps in handling them can cause silent failures, unhandled rejections, and hard-to-debug crashes.

This guide covers every pattern for handling errors in modern async JavaScript and TypeScript.

The Basics: Try-Catch with Async/Await

async function fetchUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) {
      throw new Error(`HTTP ${res.status}: ${res.statusText}`);
    }
    return await res.json();
  } catch (err) {
    console.error('Failed to fetch user:', err);
    throw err;  // re-throw so callers can handle it
  }
}

The await keyword "unwraps" a rejected promise into a thrown error. Without await, you're working with a Promise object and errors won't be caught by try-catch.

Common mistake:

// ❌ Error is NOT caught — missing await
async function buggy() {
  try {
    fetch('/api/data')  // Promise returned but not awaited
      .then(res => res.json());  // rejection here won't be caught
  } catch (err) {
    // This never runs for fetch errors
  }
}

// ✅ Fixed
async function correct() {
  try {
    const res = await fetch('/api/data');
    return await res.json();
  } catch (err) {
    // Catches both network errors and response parsing errors
  }
}

Error Types to Handle

async function robustFetch(url) {
  try {
    const res = await fetch(url);

    // HTTP errors (4xx, 5xx) don't throw by default — you must check
    if (!res.ok) {
      const body = await res.text();
      throw new HttpError(res.status, res.statusText, body);
    }

    return await res.json();
  } catch (err) {
    if (err instanceof HttpError) {
      // Handle server error (404, 500, etc.)
      throw err;
    }
    if (err instanceof TypeError && err.message.includes('fetch')) {
      // Network error (offline, DNS failure, CORS)
      throw new NetworkError('Network unavailable', { cause: err });
    }
    if (err instanceof SyntaxError) {
      // JSON parse error
      throw new ParseError('Invalid JSON response', { cause: err });
    }
    throw err;  // Unknown error — re-throw
  }
}

class HttpError extends Error {
  constructor(status, statusText, body) {
    super(`HTTP ${status}: ${statusText}`);
    this.name = 'HttpError';
    this.status = status;
    this.body = body;
  }
}

Promise.all vs Promise.allSettled

Promise.all rejects immediately when any promise rejects — other promises are abandoned:

// ❌ If any request fails, all results are lost
const [users, posts, comments] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
  fetchComments(),
]);

Promise.allSettled waits for all promises and gives you each result individually:

// ✅ Get whatever succeeded, handle whatever failed
const results = await Promise.allSettled([
  fetchUsers(),
  fetchPosts(),
  fetchComments(),
]);

const users = results[0].status === 'fulfilled' ? results[0].value : [];
const posts = results[1].status === 'fulfilled' ? results[1].value : [];

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

if (errors.length > 0) {
  console.warn('Some requests failed:', errors);
}

Use Promise.all when all requests must succeed. Use Promise.allSettled when partial success is acceptable.

Timeout Pattern

fetch doesn't have a built-in timeout. Use AbortController:

async function fetchWithTimeout(url, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    return await res.json();
  } catch (err) {
    if (err.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    throw err;
  } finally {
    clearTimeout(timeoutId);  // always clean up
  }
}

Or use Promise.race:

function timeout(ms) {
  return new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
  );
}

async function fetchWithRaceTimeout(url, ms = 5000) {
  return Promise.race([
    fetch(url).then(r => r.json()),
    timeout(ms),
  ]);
}

Retry Logic with Exponential Backoff

async function fetchWithRetry(url, options = {}) {
  const { maxRetries = 3, baseDelay = 1000, retryOn = [429, 503] } = options;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const res = await fetch(url, options);

      if (retryOn.includes(res.status) && attempt < maxRetries) {
        const delay = baseDelay * 2 ** attempt + Math.random() * 1000;
        console.log(`Retrying in ${Math.round(delay)}ms (attempt ${attempt + 1})`);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();

    } catch (err) {
      if (attempt === maxRetries) throw err;

      const isRetryable = err.name === 'TypeError';  // network errors
      if (!isRetryable) throw err;

      const delay = baseDelay * 2 ** attempt;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Avoiding Unhandled Promise Rejections

An unhandled promise rejection crashes Node.js (since v15) and logs warnings in browsers.

// ❌ Unhandled rejection — no .catch() and no await with try-catch
const promise = fetchData();
// promise might reject, but nothing handles it

// ✅ Handle it
fetchData().catch(err => console.error('fetchData failed:', err));

// ✅ Or with async/await
async function init() {
  try {
    await fetchData();
  } catch (err) {
    console.error('fetchData failed:', err);
  }
}

// ✅ Global handler as last resort
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection:', reason);
  // Don't swallow — log and optionally exit
  process.exit(1);
});

TypeScript: Typed Error Handling

TypeScript doesn't type errors in catch blocks — they're always unknown:

async function fetchData(url: string): Promise<Data> {
  try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json() as Data;
  } catch (err) {
    // err is 'unknown' in TypeScript strict mode
    if (err instanceof Error) {
      throw new Error(`Failed to fetch: ${err.message}`);
    }
    throw new Error('Unknown error');
  }
}

Result pattern (no exceptions):

type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

async function safeFetch<T>(url: string): Promise<Result<T>> {
  try {
    const res = await fetch(url);
    if (!res.ok) {
      return { ok: false, error: new Error(`HTTP ${res.status}`) };
    }
    const data = await res.json() as T;
    return { ok: true, value: data };
  } catch (err) {
    return { ok: false, error: err instanceof Error ? err : new Error(String(err)) };
  }
}

// Usage — no try-catch at call site
const result = await safeFetch<User>('/api/user/1');
if (!result.ok) {
  console.error('Failed:', result.error.message);
  return;
}
const user = result.value;  // typed as User

Error Boundary in React

For async errors in React components, use error boundaries:

// React Query handles async errors gracefully
const { data, error, isLoading } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  retry: 3,
  retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
});

if (error) {
  return <ErrorMessage message={error.message} />;
}

Quick Reference

Scenario Solution
Single async operation try/catch with await
Multiple operations, all must succeed Promise.all + try/catch
Multiple operations, partial failure OK Promise.allSettled
Timeout needed AbortController + timeout
Transient failures (rate limits, 503) Retry with exponential backoff
Prevent unhandled rejections Always .catch() or await in try/catch

→ Use the JSON Viewer to inspect API error responses and understand their structure.