Proper async error handling can make or break your technical interview especially for SDE-2, SDE-3, and Senior Frontend roles at top product companies
After going through multiple technical interviews as a senior developer, I’ve observed a consistent pattern: even experienced JavaScript developers often fumble when it comes to handling errors with Promises. It’s not merely about syntax… it’s the subtle understanding of how and when to catch errors that really distinguishes a senior engineer from a junior one.
The Interview Reality Check
Here's what typically happens: An interviewer asks you to fetch user data and handle potential errors. Most candidates write something like this:
// ❌ The code that gets you rejected
function getUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(data => {
console.log('User:', data);
return data;
})
.catch(error => {
console.log('Error:', error);
});
}
Why this fails the interview:
- No HTTP status code checking
- Swallowing errors instead of propagating them
- Poor error messaging
- No recovery strategy
The Foundation: Understanding Promise Error Flow
Before we dive into solutions, let's understand how errors flow through Promise chains. This is where most developers trip up in interviews.
The Three Types of Promise Errors
1. Rejection Errors: When a Promise explicitly rejects
const explicitReject = new Promise((resolve, reject) => {
reject(new Error('Something went wrong'));
});
2. Thrown Errors: When code throws inside a Promise
const thrownError = new Promise((resolve) => {
throw new Error('Oops!'); // This becomes a rejection
});
3. Async Function Errors: When async functions throw
async function asyncError() {
throw new Error('Async error'); // Returns rejected Promise
}
Interview Tip: Explain that all three scenarios result in rejected Promises. This shows you understand the underlying mechanics.
The Right Way: Comprehensive Error Handling
Here's how a senior developer handles the same scenario:
// ✅ The code that gets you hired
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
// Check if the request was successful
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status} - ${response.statusText}`);
}
const data = await response.json();
// Validate the data structure
if (!data || typeof data.id === 'undefined') {
throw new Error('Invalid user data received');
}
return data;
} catch (error) {
// Log for debugging but don't expose internal errors
console.error('Failed to fetch user data:', error.message);
// Throw a user-friendly error
throw new Error(`Unable to load user information. Please try again.`);
}
}
Why this impresses interviewers:
- HTTP status validation
- Data validation
- Proper error propagation
- User-friendly error messages
- Separation of concerns (logging vs user feedback)
Advanced Pattern: Error Classification
Senior developers classify errors to handle them appropriately:
class APIError extends Error {
constructor(message, status, isRetryable = false) {
super(message);
this.name = 'APIError';
this.status = status;
this.isRetryable = isRetryable;
}
}
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
async function fetchUserWithRetry(userId, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(`/api/users/${userId}`);
if (response.status >= 500) {
throw new APIError('Server error', response.status, true);
}
if (response.status >= 400) {
throw new APIError('Client error', response.status, false);
}
const data = await response.json();
if (!data.id) {
throw new ValidationError('Missing user ID', 'id');
}
return data;
} catch (error) {
attempt++;
if (error instanceof APIError && error.isRetryable && attempt < maxRetries) {
console.warn(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
continue;
}
// Re-throw if not retryable or max attempts reached
throw error;
}
}
}
Promise.all() Error Handling: The Interview Trap
This is where most candidates fail spectacularly:
// ❌ Wrong approach - fails fast and loses all data
async function loadDashboard(userId) {
try {
const [user, posts, notifications] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchNotifications(userId)
]);
return { user, posts, notifications };
} catch (error) {
throw new Error('Dashboard failed to load');
}
}
The problem: If any request fails, you lose everything.
// ✅ Correct approach - graceful degradation
async function loadDashboard(userId) {
const results = await Promise.allSettled([
fetchUser(userId),
fetchPosts(userId),
fetchNotifications(userId)
]);
const [userResult, postsResult, notificationsResult] = results;
// Handle critical failures
if (userResult.status === 'rejected') {
throw new Error('Failed to load user data');
}
return {
user: userResult.value,
posts: postsResult.status === 'fulfilled' ? postsResult.value : [],
notifications: notificationsResult.status === 'fulfilled' ? notificationsResult.value : [],
hasErrors: results.some(r => r.status === 'rejected')
};
}
The async/await vs .then() Error Handling Debate
Interviewers often ask: "When would you use .then() over async/await for error handling?"
Here's a practical scenario that reveals the key difference:
Scenario: User Registration with Error Recovery
Using .then() for error transformation pipeline:
// ✅ .then() excels when you need different error handling at each step
function registerUser(userData) {
return validateEmail(userData.email)
.then(() => checkEmailExists(userData.email))
.catch(ValidationError, () => {
throw new Error('Please enter a valid email address');
})
.catch(DuplicateEmailError, () => {
throw new Error('An account with this email already exists');
})
.then(() => createAccount(userData))
.catch(DatabaseError, () => {
throw new Error('Registration is temporarily unavailable');
});
}
Using async/await for the same logic:
// ❌ async/await makes this more verbose and harder to follow
async function registerUser(userData) {
try {
await validateEmail(userData.email);
await checkEmailExists(userData.email);
return await createAccount(userData);
} catch (error) {
if (error instanceof ValidationError) {
throw new Error('Please enter a valid email address');
} else if (error instanceof DuplicateEmailError) {
throw new Error('An account with this email already exists');
} else if (error instanceof DatabaseError) {
throw new Error('Registration is temporarily unavailable');
} else {
throw error;
}
}
}
The key insight: Use .then() when you need to transform different types of errors into user-friendly messages. Use async/await when you have complex conditional logic that depends on intermediate results.
Interview tip: Explain that .then() creates a clear error transformation pipeline, while async/await is better for imperative, step-by-step logic.
Interview Questions That Reveal Expertise
Question 1: "How do you handle errors in Promise chains?"
Red flag answer: "I just add .catch() at the end"
Green flag answer:
- Explain error bubbling through the chain
- Discuss when to catch early vs. late
- Show understanding of error transformation
- Mention the importance of not swallowing errors
Question 2: "What happens to unhandled Promise rejections?"
Red flag answer: "They get ignored"
Green flag answer:
- Explain the
unhandledRejection
event - Discuss how Node.js vs browsers handle them differently
- Show awareness of future plans to terminate processes on unhandled rejections
- Demonstrate how to add global handlers
// Node.js
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Application specific logging, throwing an error, or other logic here
});
// Browser
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled promise rejection:', event.reason);
event.preventDefault(); // Prevent the default browser behavior
});
Question 3: "How do you test Promise error handling?"
Show testing knowledge:
// Jest example
describe('getUserData', () => {
test('should handle network errors gracefully', async () => {
fetch.mockRejectedValue(new Error('Network error'));
await expect(getUserData(123)).rejects.toThrow('Unable to load user information');
});
test('should handle HTTP errors', async () => {
fetch.mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found'
});
await expect(getUserData(123)).rejects.toThrow('HTTP Error: 404');
});
});
Common Interview Mistakes to Avoid
1. Swallowing Errors
// ❌ Don't do this
.catch(error => {
console.log(error);
return null; // Swallows the error
});
// ✅ Do this instead
.catch(error => {
console.error('Operation failed:', error);
throw new Error('User-friendly error message');
});
2. Not Validating HTTP Responses
// ❌ fetch() doesn't reject on HTTP errors
const response = await fetch(url);
const data = await response.json(); // Could fail if response is HTML error page
// ✅ Always check response.ok
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
3. Mixing Error Handling Styles
// ❌ Inconsistent error handling
async function badExample() {
try {
const result = await someOperation()
.catch(error => {
console.log(error);
return null;
});
return result;
} catch (error) {
// This won't catch the error above!
throw error;
}
}
Key Takeaways for Your Next Interview
- Always validate HTTP responses : fetch() doesn't reject on 404 or 500
- Classify errors : Different errors need different handling strategies
- Don't swallow errors : Log them, transform them, but don't hide them
- Use appropriate error handling patterns : Promise.allSettled() for parallel operations that can partially fail
- Think about the user experience : Provide meaningful error messages
- Consider retry strategies : For transient failures
- Test your error handling : Show you understand how to write tests for failure scenarios
The Bottom Line
Error handling in JavaScript Promises isn't just about adding .catch()
blocks. It's about understanding error flow, building resilient systems, and providing great user experiences even when things go wrong.
The next time you're in a technical interview and the conversation turns to error handling, remember: this is your chance to show you think like a senior developer. Don't just write code that works, write code that fails gracefully.
Have you encountered tricky Promise error handling scenarios in interviews?
Top comments (0)