Promise Error Handling

Handle rejected promises explicitly, decide whether partial results are useful, and expose a recoverable error state without leaving stale loading indicators.

A promise represents an eventual result that can either fulfill or reject. In frontend code, rejection commonly comes from failed network requests, invalid responses, or other asynchronous operations, so callers should decide how those failures affect the UI.

Use `try...catch` with `await` when you want synchronous-looking control flow. A rejection thrown by an awaited promise enters the nearest matching `catch`, where you can update error state, log useful diagnostics, retry, or provide fallback data.

With promise chaining, handle failures using `.catch()`. A rejection can also be handled by the second argument to `.then()`, but `.catch()` is generally clearer because it can handle failures from the preceding chain as well.

Loading state should be cleared regardless of success or failure. A `finally` block is useful for cleanup such as setting `loading` to `false`, while the `catch` block handles the user-visible error state.

When several requests run together, choose an aggregation method deliberately. `Promise.all()` rejects when any input rejects, while alternatives such as `Promise.allSettled()` let you inspect every result and can support UIs where partial data remains useful.

Key points

Interview questions

How do you handle a rejected promise in JavaScript?

With `async`/`await`, wrap the awaited operation in `try...catch`. The `catch` block can update UI error state, log diagnostics, or return a fallback. With promise chaining, use `.catch()` to handle rejection. The important part is to make an explicit decision about the failure rather than silently ignoring it.

Does `fetch()` reject when the server returns a 404 or 500?

No, not merely because the HTTP status indicates an error. `fetch()` normally fulfills with a `Response` object for HTTP error statuses. Code should check `response.ok` or inspect `response.status` and throw or otherwise handle the response when appropriate.

Why is `finally` useful when managing loading state?

Loading state usually needs to end whether the asynchronous operation succeeds or fails. Putting cleanup in `finally` avoids duplicating the same state update in both `try` and `catch`. This helps prevent a UI from remaining stuck in a loading state after an error.

When would you use `Promise.allSettled()` instead of `Promise.all()`?

Use `Promise.all()` when the combined operation should be considered unsuccessful if any required operation fails. Use `Promise.allSettled()` when requests are independent and you want the outcome of every request. This is useful for dashboards or pages where some sections can still render even if another request fails.

How would you design a recoverable UI for a failed asynchronous request?

Represent loading, data, and error as explicit state rather than assuming success. On failure, stop the loading indicator, preserve usable existing data when appropriate, and expose a clear retry action. The request logic should also prevent stale or obsolete requests from overwriting newer state. The exact design depends on whether the operation is idempotent and whether partial or cached data is safe to display.