Learning Objectives

  • Master error catching in Promise chains
  • Understand error propagation
  • Learn error recovery patterns
  • Implement best practices for error handling

Why Error Handling Matters

Unhandled Promise rejections can crash your application or lead to silent failures. Proper error handling ensures:

Basic Error Catching

Using .catch()

fetchUser(1)
  .then(user => console.log(user))
  .catch(error => {
    console.error('Failed to fetch user:', error.message);
  });

Catching Errors in Chains

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);
  });

Error Propagation

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"
  });

Multiple .catch() Handlers

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);
  });

Error Recovery Patterns

1. Fallback Values

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);
  });

2. Retry Logic

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));

3. Partial Success Handling

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);
});

Re-throwing Errors

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);
  });

Custom Error Types

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);
    }
  });

Unhandled Rejection Tracking

Warning: Always handle Promise rejections to avoid unhandled rejection warnings!

Global Handlers

// 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();
});

Best Practices

1. Always Add .catch()

// ❌ 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));

2. Catch at the Right Level

// 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);
  });

3. Provide Meaningful Error Messages

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}`);
    });
}

4. Clean Up Resources

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();
    }
  });

Key Takeaways

  • ✅ Always use .catch() to handle errors
  • ✅ Errors propagate down the chain until caught
  • ✅ Use .catch() for recovery with fallback values
  • ✅ Re-throw errors when needed to propagate them
  • ✅ Use custom error types for better error handling
  • ✅ Set up global handlers for unhandled rejections
  • ✅ Use .finally() for cleanup operations

Next Steps

Now that you've mastered Promise fundamentals, let's explore Promise static methods like Promise.all()!