JavaScript Promises Tutorial - Section 2: Fundamentals
Unhandled Promise rejections can crash your application or lead to silent failures. Proper error handling ensures:
fetchUser(1)
.then(user => console.log(user))
.catch(error => {
console.error('Failed to fetch user:', error.message);
});
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.catch(error => {
// Catches errors from ANY step above
console.error('Error in chain:', error);
});
Errors automatically propagate down the chain until caught:
Promise.resolve(1)
.then(x => {
throw new Error('Step 1 failed');
})
.then(x => {
console.log('Step 2'); // Skipped
})
.then(x => {
console.log('Step 3'); // Skipped
})
.catch(error => {
console.error('Caught:', error.message); // "Step 1 failed"
});
You can have multiple .catch() handlers for different error scenarios:
fetchUser(1)
.then(user => {
if (!user.isActive) {
throw new Error('User is inactive');
}
return user;
})
.catch(error => {
console.error('User fetch error:', error);
return { id: 1, name: 'Guest', isActive: true }; // Fallback
})
.then(user => fetchPosts(user.id))
.catch(error => {
console.error('Posts fetch error:', error);
return []; // Return empty array on error
})
.then(posts => {
console.log('Posts:', posts);
});
fetchUserFromPrimaryDB(userId)
.catch(error => {
console.log('Primary DB failed, trying backup');
return fetchUserFromBackupDB(userId);
})
.catch(error => {
console.log('Backup DB failed, using default');
return { id: userId, name: 'Unknown User' };
})
.then(user => {
console.log('User:', user);
});
function fetchWithRetry(url, retries = 3) {
return fetch(url)
.then(response => response.json())
.catch(error => {
if (retries > 0) {
console.log(`Retrying... (${retries} left)`);
return fetchWithRetry(url, retries - 1);
}
throw error;
});
}
fetchWithRetry('/api/data')
.then(data => console.log(data))
.catch(error => console.error('All retries failed:', error));
Promise.all([
fetchUser(1).catch(e => ({ error: e.message })),
fetchUser(2).catch(e => ({ error: e.message })),
fetchUser(3).catch(e => ({ error: e.message }))
])
.then(results => {
const successful = results.filter(r => !r.error);
const failed = results.filter(r => r.error);
console.log('Successful:', successful.length);
console.log('Failed:', failed.length);
});
Sometimes you want to log an error but still propagate it:
fetchUser(1)
.then(user => fetchPosts(user.id))
.catch(error => {
console.error('Error occurred:', error);
logErrorToService(error);
throw error; // Re-throw to propagate
})
.then(posts => {
console.log('This won\'t run if error was thrown');
})
.catch(error => {
console.error('Final handler:', error);
});
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
}
}
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
function fetchUser(id) {
if (id <= 0) {
return Promise.reject(
new ValidationError('Invalid user ID', 'id')
);
}
return fetch(`/api/users/${id}`)
.then(response => {
if (!response.ok) {
throw new NetworkError(
'Failed to fetch user',
response.status
);
}
return response.json();
});
}
fetchUser(-1)
.catch(error => {
if (error instanceof ValidationError) {
console.error('Validation error:', error.field);
} else if (error instanceof NetworkError) {
console.error('Network error:', error.statusCode);
} else {
console.error('Unknown error:', error);
}
});
Warning: Always handle Promise rejections to avoid unhandled rejection warnings!
// Node.js
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise);
console.error('Reason:', reason);
// Log to error tracking service
});
// Browser
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled rejection:', event.reason);
// Log to error tracking service
event.preventDefault();
});
// ❌ Bad: No error handling
fetchUser(1).then(user => console.log(user));
// ✅ Good: Error handling
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error(error));
// Catch at the end of the chain
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => processPosts(posts))
.catch(error => {
// Handles all errors in the chain
console.error('Error:', error);
});
function fetchUser(id) {
return fetch(`/api/users/${id}`)
.then(response => {
if (!response.ok) {
throw new Error(
`Failed to fetch user ${id}: ${response.status}`
);
}
return response.json();
})
.catch(error => {
throw new Error(`User fetch error: ${error.message}`);
});
}
let connection;
openDatabaseConnection()
.then(conn => {
connection = conn;
return queryDatabase(connection);
})
.then(results => {
return processResults(results);
})
.catch(error => {
console.error('Database error:', error);
throw error;
})
.finally(() => {
if (connection) {
connection.close();
}
});
.catch() to handle errors.catch() for recovery with fallback values.finally() for cleanup operationsNow that you've mastered Promise fundamentals, let's explore Promise static methods like Promise.all()!