Rejected Promises in TypeScript: A Complete Guide to Error Handling

Learn how to properly handle promise rejections with TypeScript's type-safe patterns, from basic try/catch to advanced retry strategies and circuit breakers.

Understanding Promise Rejection in TypeScript

Promise rejections are an inevitable part of asynchronous programming in TypeScript. When a promise fails--whether due to network errors, validation failures, or unexpected system conditions--it enters a "rejected" state that, if not handled properly, can crash your application or create silent failures that are impossible to debug.

Unlike synchronous exceptions that throw immediately and can be caught with try/catch blocks, promise rejections require a different approach. TypeScript's type system provides powerful tools to make error handling more predictable and maintainable.

What Causes a Promise to Reject

A promise rejects when an asynchronous operation fails. The Promise constructor accepts an executor function with two parameters: resolve and reject. When you call reject() with any value, the promise transitions to the rejected state.

Common rejection scenarios include:

  • Network request failures -- API unavailable, timeout, DNS resolution errors
  • File system operations -- Missing files, permission denied, disk full conditions
  • Validation errors -- Invalid user input, schema violations in async operations
  • Authentication failures -- Expired tokens, insufficient permissions
  • Third-party service errors -- Payment gateway failures, email service downtime
  • Resource exhaustion -- Memory limits, connection pool exhaustion

Each of these scenarios requires different handling strategies. A network timeout might warrant a retry, while a validation error should surface to the user immediately. TypeScript's type system helps distinguish between these cases and handle each appropriately. Understanding these patterns is essential for building robust web applications that gracefully handle failures.

For applications leveraging React Server Components, proper promise handling becomes even more critical since async operations cascade through the component tree.

Type-Safe Error Handling Patterns

Master these essential patterns for robust promise rejection handling

try/catch with Async/Await

The most common pattern that mirrors synchronous error handling, with TypeScript's type narrowing capabilities for precise error handling.

Promise Chain .catch()

Handle errors at any point in a promise chain with the .catch() method, enabling recovery and continued execution.

Custom Error Types

Create typed error classes that carry additional context about failures, enabling appropriate handling strategies.

Result Types

Explicit success/failure types that make all possible outcomes visible in function signatures, eliminating hidden error paths.

Type-Safe try/catch with Async/Await
1async function getUserData(userId: string): Promise<UserData> {2 try {3 const response = await fetch(`/api/users/${userId}`);4 5 if (!response.ok) {6 throw new ApiError('Failed to fetch user', response.status);7 }8 9 return response.json();10 } catch (error) {11 // TypeScript's type narrowing helps handle different error types12 if (error instanceof ApiError) {13 logger.error('API error fetching user', { userId, status: error.status });14 throw error;15 }16 17 if (error instanceof TypeError) {18 logger.error('Network error fetching user', { userId, error: error.message });19 throw new NetworkError('Unable to connect to server');20 }21 22 logger.error('Unknown error fetching user', { userId, error });23 throw new Error('An unexpected error occurred');24 }25}

Retry and Recovery Strategies

Network operations and external service calls frequently fail transiently. Implementing retry logic with exponential backoff dramatically improves resilience for these scenarios.

Implementing Retry Logic

The retry pattern attempts an operation multiple times with increasing delays between attempts. This approach handles temporary failures like network glitches or momentary service unavailability. The key components include exponential backoff--where delays grow geometrically--and jitter, which adds randomization to prevent the thundering herd problem when multiple clients retry simultaneously.

A configurable retry condition allows you to specify which errors are retryable. Network timeouts and 503 errors might warrant another attempt, while validation errors (400) or authentication failures (401) should fail immediately. This selective retrying prevents wasted resources on errors that won't resolve themselves.

Circuit Breaker Pattern

For services experiencing prolonged outages, continuous retry attempts waste resources and can make the situation worse. The circuit breaker pattern prevents repeated attempts after a threshold of failures, allowing the external service time to recover.

The circuit breaker operates in three states: closed (normal operation where all requests pass through), open (failing fast without attempting the operation after too many failures), and half-open (allowing a single test request through to check if the service recovered). When failures exceed the threshold, the breaker opens and immediately rejects requests for a configurable timeout duration. After the timeout, the half-open state permits a single request--if it succeeds, the breaker closes and resumes normal operation.

Graceful Degradation

When external services are unavailable and retries or circuit breakers won't help, graceful degradation allows your application to continue functioning with reduced capabilities rather than failing completely. This pattern is particularly valuable for non-critical features.

The stale cache pattern provides an excellent example: when fresh data can't be fetched, serve cached data even if slightly outdated. A dashboard that can't load real-time analytics can display cached data from the last successful fetch. The key is identifying which features are essential versus nice-to-have and implementing appropriate fallbacks for each. This approach maintains user experience even when dependencies experience issues, keeping your application reliable across various conditions.

For applications sending transactional emails via services like Nodemailer, implementing proper retry and circuit breaker patterns ensures reliable delivery even when email services experience temporary issues.

Retry Logic with Exponential Backoff
1async function withRetry<T>(2 operation: () => Promise<T>,3 options: {4 maxAttempts?: number;5 initialDelay?: number;6 maxDelay?: number;7 backoffMultiplier?: number;8 retryCondition?: (error: unknown) => boolean;9 } = {}10): Promise<T> {11 const {12 maxAttempts = 3,13 initialDelay = 1000,14 maxDelay = 30000,15 backoffMultiplier = 2,16 retryCondition = () => true17 } = options;18 19 let lastError: unknown;20 21 for (let attempt = 1; attempt <= maxAttempts; attempt++) {22 try {23 return await operation();24 } catch (error) {25 lastError = error;26 27 if (attempt === maxAttempts) throw lastError;28 if (!retryCondition(error)) throw error;29 30 // Calculate delay with exponential backoff and jitter31 const baseDelay = Math.min(32 initialDelay * Math.pow(backoffMultiplier, attempt - 1),33 maxDelay34 );35 const jitter = Math.random() * baseDelay * 0.3;36 const delay = baseDelay + jitter;37 38 await sleep(delay);39 }40 }41 42 throw lastError;43}

Global Error Handling

Unhandled promise rejections can crash your application. Process-level and browser-specific handlers provide safety nets for catching any rejections that slip through your application's error handling.

Node.js Process-Level Handlers

In Node.js environments, unhandled promise rejections can crash your entire process. The unhandledRejection event captures any rejection that doesn't have an associated handler, while uncaughtException handles synchronous errors that would otherwise crash the process. Both handlers should log detailed information and initiate graceful shutdown when appropriate--closing database connections, finishing in-flight requests, and alerting monitoring systems before exiting.

Implementing graceful shutdown patterns ensures your application can recover from unexpected failures. This includes setting a timeout for shutdown operations and forcing exit if the application doesn't close cleanly. These patterns are essential for production deployments where reliability matters.

Browser-Specific Global Handlers

In browser environments, unhandled promise rejections trigger the unhandledrejection event that you can listen for via window.addEventListener(). Modern browsers provide more sophisticated handling than in the past, but explicit handlers are still valuable for logging to monitoring services and providing appropriate user notifications.

The decision of whether to notify users depends on the error type--network failures are often temporary and don't need user notification, while unexpected JavaScript errors might warrant showing a friendly error message. Integrating with error tracking services like Sentry, Rollbar, or Datadog helps aggregate these errors for analysis.

Structured Logging for Async Errors

Effective debugging requires structured logging that captures the context of async failures. Simply logging an error message is rarely sufficient--you need to understand what operation was attempted, what data was involved, and what the user was doing when the error occurred.

Structured logging includes consistent context fields: operation name, user ID, request ID, timestamps, and relevant metadata. When every log entry follows this format, you can trace failures through complex async flows and understand the full context of any error. Modern logging systems like Winston, Pino, or cloud-native solutions make this approach straightforward to implement.

Performance Considerations

Memory Management for Long-Running Promises

Unhandled promise rejections can cause memory leaks, especially in long-running applications. Promises hold references to their resolution values and any handlers attached to them. If promises never settle due to unhandled rejections, these references can prevent garbage collection and gradually consume available memory.

Implementing a promise pool pattern limits concurrent operations and automatically cleans up stalled promises. By tracking when promises were created, their context, and enforcing maximum ages, you can identify leaks before they impact performance. Regular monitoring of memory usage helps catch issues early in production environments.

Avoiding Promise Chain Performance Pitfalls

Promise chains, while powerful, can introduce performance issues if not used carefully. Each .then() creates a new promise, and deeply nested chains can exhaust the event loop's microtask queue. Understanding when to use sequential versus parallel execution is crucial.

For independent operations, Promise.all() executes all promises in parallel for better performance. However, when operations must happen in order or when rate limiting is needed, sequential execution via await in a loop is appropriate. For APIs with rate limits, batched parallel execution provides the best balance--processing multiple items concurrently while respecting API constraints.

The Promise.allSettled() method is particularly useful when you want all operations to complete regardless of individual failures, allowing you to handle partial successes and failures gracefully. This pattern is essential for building scalable applications that handle varying failure scenarios efficiently.

When working with JavaScript's event loop and callback patterns, understanding how this context behaves in callbacks helps avoid common pitfalls that lead to unexpected rejections and difficult-to-debug issues.

Frequently Asked Questions

Build Resilient TypeScript Applications

Our team of experienced developers specializes in building robust, error-handling applications that gracefully handle failures and deliver reliable user experiences.