Docs
README
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
| Cause | Example | Solution |
|---|---|---|
| Missing .catch() | promise.then(...) | Add .catch(handler) |
| Missing try/catch | await promise | Wrap in try/catch |
| Fire-and-forget | doAsync() | Add handler or use .catch() |
| Conditional catch | if (x) await p | Always 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
| Practice | Description |
|---|---|
| Always handle rejections | Every promise should have error handling |
| Fail fast when appropriate | Use Promise.all for dependent operations |
| Use allSettled for independent ops | Don't let one failure block others |
| Classify errors | Use custom error types for better handling |
| Clean up resources | Use finally to ensure cleanup |
| Log errors centrally | Use global handlers as safety net |
| Transform errors | Add context as errors propagate |
| Avoid swallowing errors | Don'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
- ā¢Async errors don't propagate automatically - must be caught explicitly
- ā¢Use try/catch with async/await - familiar synchronous-style handling
- ā¢Use .catch() with promises - for chain-style error handling
- ā¢Promise.allSettled for graceful degradation - when partial success is acceptable
- ā¢Always clean up resources - use
finallyor try/finally patterns - ā¢Classify and transform errors - makes handling more precise
- ā¢Global handlers are safety nets - not replacements for proper handling