DEV Community

Bhoomika Chauhan
Bhoomika Chauhan

Posted on

Mastering Promise Error Handling: What Every Developer Gets Wrong in Interviews

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);
    });
}
Enter fullscreen mode Exit fullscreen mode

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'));
});
Enter fullscreen mode Exit fullscreen mode

2. Thrown Errors: When code throws inside a Promise

const thrownError = new Promise((resolve) => {
  throw new Error('Oops!'); // This becomes a rejection
});
Enter fullscreen mode Exit fullscreen mode

3. Async Function Errors: When async functions throw

async function asyncError() {
  throw new Error('Async error'); // Returns rejected Promise
}
Enter fullscreen mode Exit fullscreen mode

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.`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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')
  };
}
Enter fullscreen mode Exit fullscreen mode

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');
    });
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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');
  });
});
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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}`);
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways for Your Next Interview

  1. Always validate HTTP responses : fetch() doesn't reject on 404 or 500
  2. Classify errors : Different errors need different handling strategies
  3. Don't swallow errors : Log them, transform them, but don't hide them
  4. Use appropriate error handling patterns : Promise.allSettled() for parallel operations that can partially fail
  5. Think about the user experience : Provide meaningful error messages
  6. Consider retry strategies : For transient failures
  7. 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)