Asynchronous error handling
Challenges of Asynchronous Error Handling
JavaScript's asynchronous nature makes error handling complex. Callback functions, Promises, and async/await each have different error handling mechanisms. Uncaught asynchronous errors can cause silent failures in programs, and this stealthiness makes debugging difficult.
// Typical example of unhandled asynchronous error
setTimeout(() => {
throw new Error('Asynchronous error not caught');
}, 1000);
Error Handling in Callback Functions
In the traditional callback pattern, errors are typically passed as the first parameter. This conventional pattern is called "error-first callbacks."
fs.readFile('nonexistent-file.txt', (err, data) => {
if (err) {
console.error('Error reading file:', err.message);
return;
}
console.log('File content:', data);
});
Common issues include forgetting to check the error parameter, synchronous errors thrown in callbacks not being caught, and nested callbacks leading to "callback hell," which scatters error handling logic.
Promise Error Handling Mechanism
Promises provide a more structured way to handle errors. Errors can be caught using the .catch()
method or the second parameter of .then()
.
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not OK');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => {
console.error('Request failed:', error);
});
Errors in a Promise chain propagate downward until they encounter a .catch()
handler. Unhandled Promise rejections result in an UnhandledPromiseRejectionWarning
.
// Unhandled Promise rejection
new Promise((resolve, reject) => {
reject(new Error('This error is not caught'));
});
Error Handling in async/await
The async/await syntax makes asynchronous code look synchronous, but error handling still requires special attention. The most common approach is using try/catch blocks.
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Failed to fetch data:', error);
}
}
For parallel operations, error handling requires more nuance:
async function fetchMultiple() {
try {
const [res1, res2] = await Promise.all([
fetch('https://api.example.com/data1'),
fetch('https://api.example.com/data2')
]);
// Process results...
} catch (error) {
// Any failed request will end up here
console.error('Request failed:', error);
}
}
Global Error Handling
For uncaught asynchronous errors, global handlers can be set up:
// Node.js environment
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Promise rejection:', reason);
});
// Browser environment
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled Promise rejection:', event.reason);
event.preventDefault(); // Prevent default error output
});
Error Boundaries and Propagation
In complex applications, error propagation strategies must be considered. Sometimes errors should be handled locally, while other times they should bubble up to higher levels.
async function processUserData(userId) {
try {
const user = await getUser(userId);
const profile = await getProfile(user.id);
return { user, profile };
} catch (error) {
if (error instanceof NetworkError) {
// Special handling for network errors
await logNetworkError(error);
throw new RetryableError('Handling retryable error');
}
throw error; // Propagate other errors upward
}
}
Error Types and Custom Errors
Creating specific error types enables more precise error handling:
class ApiError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.name = 'ApiError';
}
}
async function callApi() {
try {
const response = await fetch('/api');
if (!response.ok) {
throw new ApiError('API request failed', response.status);
}
return await response.json();
} catch (error) {
if (error instanceof ApiError && error.statusCode === 404) {
// Special handling for 404 errors
return fallbackData;
}
throw error;
}
}
Error Logging and Monitoring
In production environments, errors should be logged to monitoring systems:
async function logError(error) {
try {
await fetch('/api/log-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
})
});
} catch (loggingError) {
console.error('Failed to log error:', loggingError);
}
}
window.addEventListener('error', event => {
logError(event.error);
});
window.addEventListener('unhandledrejection', event => {
logError(event.reason);
});
Testing Asynchronous Errors
When writing tests, verify that errors are properly thrown and handled:
// Example using Jest testing framework
describe('Async function error handling', () => {
test('should throw specific error', async () => {
await expect(failingAsyncFunction()).rejects.toThrow(ExpectedError);
});
test('should handle error correctly', async () => {
const result = await functionThatHandlesError();
expect(result).toBe(fallbackValue);
});
});
Performance Considerations
Error handling logic can impact performance, especially in hot paths:
// Inefficient error handling
async function processItems(items) {
const results = [];
for (const item of items) {
try {
results.push(await process(item));
} catch (error) {
results.push(null);
}
}
return results;
}
// More efficient error handling
async function processItems(items) {
return Promise.allSettled(items.map(item =>
process(item).catch(() => null)
));
}
Browser and Node.js Differences
Asynchronous error handling differs across environments:
// Web Worker error handling in browsers
const worker = new Worker('worker.js');
worker.onerror = (event) => {
console.error('Worker error:', event.message);
};
// Child process error handling in Node.js
const { spawn } = require('child_process');
const child = spawn('some-command');
child.on('error', (error) => {
console.error('Child process error:', error);
});
Third-Party Library Integration
When integrating third-party libraries, consider their error handling conventions:
// Axios error handling
axios.get('/api/data')
.then(response => console.log(response.data))
.catch(error => {
if (error.response) {
// Server responded with non-2xx status
console.error('API error:', error.response.status);
} else if (error.request) {
// Request made but no response
console.error('Network error:', error.message);
} else {
// Other errors
console.error('Configuration error:', error.message);
}
});
Error Recovery Strategies
Implement different recovery strategies based on error types:
async function withRetry(fn, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
return await fn();
} catch (error) {
attempt++;
if (attempt >= maxRetries || !isRetryable(error)) {
throw error;
}
await delay(1000 * attempt); // Exponential backoff
}
}
}
async function fetchWithRetry() {
return withRetry(() => fetch('https://api.example.com/data'));
}
Architectural Design for Error Handling
Large applications can implement centralized error handling mechanisms:
// Example error handling middleware (Express.js)
app.use(async (err, req, res, next) => {
await logError(err);
if (err instanceof ValidationError) {
return res.status(400).json({ error: err.message });
}
if (err instanceof AuthError) {
return res.status(401).json({ error: 'Unauthorized' });
}
res.status(500).json({ error: 'Server error' });
});
Frontend UI Error Handling
In frontend interfaces, display errors to users in a friendly manner:
async function loadUserProfile() {
try {
setLoading(true);
const profile = await fetchProfile();
setProfile(profile);
} catch (error) {
setError(
error instanceof NetworkError
? 'Network connection failed. Please check your connection'
: 'Error loading profile'
);
} finally {
setLoading(false);
}
}
Preventing Memory Leaks
Improper error handling can lead to memory leaks:
// Code that may cause leaks
function setupLeakyHandler() {
const hugeObject = new Array(1e6).fill('data');
someEventEmitter.on('event', () => {
throw new Error('This error is not caught');
// hugeObject won't be released
});
}
// Improved version
function setupSafeHandler() {
const hugeObject = new Array(1e6).fill('data');
someEventEmitter.on('event', () => {
try {
throw new Error('This error is caught');
} catch (error) {
console.error(error);
}
// hugeObject can be garbage collected
});
}
Error Handling and Transactions
Error handling is critical for atomic operations:
async function transferFunds(from, to, amount) {
let connection;
try {
connection = await db.getConnection();
await connection.beginTransaction();
await withdraw(from, amount, connection);
await deposit(to, amount, connection);
await connection.commit();
} catch (error) {
if (connection) {
await connection.rollback();
}
throw error;
} finally {
if (connection) {
connection.release();
}
}
}
Error Handling Pattern Comparison
Different patterns suit different scenarios:
// Pattern 1: Direct handling
async function mode1() {
try {
const result = await operation();
// Process result...
} catch (error) {
// Handle error directly
handleError(error);
}
}
// Pattern 2: Error transformation
async function mode2() {
try {
const result = await operation();
// Process result...
} catch (error) {
// Transform error type and rethrow
throw new ApplicationError('Operation failed', { cause: error });
}
}
// Pattern 3: Silent failure
async function mode3() {
try {
const result = await operation();
// Process result...
} catch (error) {
// Log but don't interrupt program flow
logError(error);
return defaultValue;
}
}
Error Handling in Async Generators
Async generator functions require special error handling:
async function* asyncGenerator() {
try {
yield await firstStep();
yield await secondStep();
} catch (error) {
console.error('Generator internal error:', error);
yield fallbackValue;
}
}
// Error handling when consuming
async function consumeGenerator() {
const generator = asyncGenerator();
try {
for await (const value of generator) {
console.log(value);
}
} catch (error) {
console.error('Error while consuming generator:', error);
}
}
WebSocket Error Handling
Real-time connections require special error handling:
function setupWebSocket(url) {
const socket = new WebSocket(url);
socket.onerror = (event) => {
console.error('WebSocket error:', event);
scheduleReconnect();
};
socket.onclose = (event) => {
if (event.wasClean) {
console.log(`Connection closed cleanly: ${event.code} ${event.reason}`);
} else {
console.error('Connection terminated abnormally');
scheduleReconnect();
}
};
let reconnectTimer;
function scheduleReconnect() {
clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
setupWebSocket(url); // Reconnect
}, 5000);
}
}
Error Handling and Performance Monitoring
Combine error handling with performance monitoring:
async function trackedOperation(operationName, fn) {
const startTime = performance.now();
try {
const result = await fn();
trackSuccess(operationName, performance.now() - startTime);
return result;
} catch (error) {
trackError(operationName, error, performance.now() - startTime);
throw error;
}
}
// Usage example
async function fetchUserData() {
return trackedOperation('fetchUserData', () => fetch('/api/user'));
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn