With Angular's HttpClient, how are a 404, a network failure and an unparseable 200 response each reported, and what does status 0 mean?
answer
- non-2xx is not a success
- one error class for all three
- zero means no HTTP answer
- check ok, status and error
basics
~20 sAll three reach the error channel as an HttpErrorResponse. A 404 keeps status 404 and the error body in error; status 0 means no HTTP response reached the app (offline, CORS block, timeout); an unparseable 200 keeps status 200 and a parsing message.
solid answer
~40 sUnlike `fetch()`, `HttpClient` treats any status outside 200-299 as a failure: the subscriber's `error` callback receives an `HttpErrorResponse` rather than a `next` value. Its fields tell the three cases apart. A **404 or 500** carries the real `status`, the `statusText`, `headers`, and the parsed error body in `error` (plain text if the body was not JSON). **Status 0** means the browser never handed the app an HTTP response: the network was down, DNS failed, CORS blocked the reply, or the request's `timeout` option expired; `error` then holds the underlying exception. A **200 whose JSON cannot be parsed** also goes to the error channel, with `status` 200, `ok` false and a message starting `Http failure during parsing`. You branch on `status` inside `catchError`, or centrally in an interceptor.
code
ts · 37 linesimport { Injectable, inject } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { Observable, catchError, map, of, throwError } from 'rxjs';
export interface Forecast {
city: string;
tempC: number;
}
export type ForecastResult =
| { kind: 'ok'; forecast: Forecast }
| { kind: 'unknown-city' }
| { kind: 'unreachable' };
@Injectable({ providedIn: 'root' })
export class WeatherService {
private readonly http = inject(HttpClient);
getForecast(city: string): Observable<ForecastResult> {
return this.http.get<Forecast>('/api/forecast', { params: { city }, timeout: 5000 }).pipe(
map((forecast): ForecastResult => ({ kind: 'ok', forecast })),
catchError((err: unknown) => {
if (err instanceof HttpErrorResponse) {
if (err.status === 0) {
// Offline, CORS block or the 5 s timeout: no HTTP response reached the app.
return of<ForecastResult>({ kind: 'unreachable' });
}
if (err.status === HttpStatusCode.NotFound) {
return of<ForecastResult>({ kind: 'unknown-city' });
}
}
// 5xx, parse failures and anything unexpected go to the caller.
return throwError(() => err);
}),
);
}
}go deeper
Know that HttpClient sends non-2xx responses to the error callback as HttpErrorResponse, and that status and error are the fields to read.
Separate the three cases precisely: real status codes, status 0 with an exception in error, and a 2xx parse failure. Explain why the error body may be JSON or text.
Diagnose status 0 in production from browser evidence rather than the error object, choose sensible timeouts, and design which failures a service maps and which it rethrows.
Define an app-wide error taxonomy so every data service reports network, client and server failures consistently to users and monitoring.
## One error type, three very different failures `HttpErrorResponse` is the class `HttpClient` uses for every failed request. It extends the same base as `HttpResponse`, so it carries `status`, `statusText`, `headers` and `url`, plus: - **`error`** — the payload of the failure: the server's error body, or the exception that stopped the request. - **`message`** — a readable summary built from the URL and status. - **`ok`** — always `false`, even when `status` is in the 2xx range. - **`name`** — the string `'HttpErrorResponse'`. It is delivered on the Observable's **error channel**, so no response value is emitted through `next`, and the stream terminates. ## Case 1: the server answered with an error status A weather API that returns `404 Not Found` for an unknown city, or `503 Service Unavailable` under load, produces an `HttpErrorResponse` with that `status`. This is the key contrast with the browser's `fetch()`, which resolves normally for a 404 or 500: `HttpClient` inspects the status for you and routes anything outside 200-299 to `error`. What `error` holds depends on the body: - With the default `responseType: 'json'`, a JSON error body is parsed, so `err.error.code` works. - A non-JSON error body (an HTML error page from a proxy, for instance) is not a parse failure; it is kept as **plain text** in `error`. - The `HttpStatusCode` enum (`HttpStatusCode.NotFound`, `HttpStatusCode.ServiceUnavailable`) avoids magic numbers. ## Case 2: status 0 — no HTTP response at all `status` is `0` when the request never produced an HTTP response the app could read. Typical causes: 1. The device is offline, DNS resolution failed, or the connection was refused or reset. 2. The browser blocked the response under **CORS**; for security the script is told nothing about the real status. 3. The request's **`timeout`** option (milliseconds) expired, so the backend aborted it. 4. The browser or an extension blocked the request, for example mixed content from an HTTPS page. In every case `error` holds the underlying exception rather than a server body. With the default **`FetchBackend`** (Angular 22) that is whatever `fetch()` rejected with — typically a `TypeError` for network and CORS failures, or a `DOMException` named `TimeoutError` when the `timeout` fired. With the XMLHttpRequest backend (`withXhr()`) a network failure carries a `ProgressEvent`, which is what older articles and the guide's wording describe; a timeout is a `DOMException` named `TimeoutError` in both backends. The status alone cannot tell you *which* cause occurred: the error object separates a timeout from the rest, and the browser console and network panel tell a CORS block from a dead network. A request cancelled by unsubscribing is not in this list: the subscriber has already left, so nothing is delivered to it. ## Case 3: a 2xx response that cannot be parsed If the server sends `200 OK` with a body that is not valid JSON while `responseType` is `'json'`, `HttpClient` cannot produce the typed body. It reports an `HttpErrorResponse` with `status` 200, `ok` false and a message beginning `Http failure during parsing for` followed by the URL. `error` holds the parse exception; the XHR backend wraps it together with the raw text. An *empty* 2xx body is not an error: it parses to `null`. ## Summary table | Failure | `status` | `error` contains | `message` starts with | |---|---|---|---| | Server error status (404, 500, 503) | The real code | Parsed JSON error body, or text | `Http failure response for` | | Network down, CORS block, timeout | `0` | The thrown exception (`TypeError`, `DOMException`, or `ProgressEvent` with XHR) | `Http failure response for` | | Invalid JSON in a 2xx response | e.g. `200` | The parse error | `Http failure during parsing for` | ## Mistakes interviewers listen for - Checking `response.ok` in the `next` callback, which is `fetch()` thinking; with `HttpClient`, a failed status never reaches `next`. - Showing "server is down" for every status 0, when the usual causes are the user's network or a CORS misconfiguration. - Reading `err.error.message` without checking whether `error` is an object, a string or an exception. - Retrying every failure, including 400 and 404 responses that will fail the same way again. ## Handling it - Catch with RxJS `catchError`, narrow with `err instanceof HttpErrorResponse`, then branch on `status`: offline and CORS problems (0) need different messages from "city not found" (404) or "try later" (503). - Rethrow what you do not handle, so a caller or a global error handler still sees it. - Mapping errors once for the whole app belongs in an interceptor rather than in every service.
- Users report a 'weather unavailable' message and your logs show HttpErrorResponse status 0 from one office network only. How do you narrow it down?Status 0 only says no HTTP response reached the app, so collect evidence the error object cannot give: the browser console (a CORS or mixed-content message), the network panel (blocked, cancelled or timed out), and whether other requests from that network succeed. One network failing points to a proxy, firewall or TLS-inspection box rather than the API itself, so compare with a direct request from that network.
- In Angular's HttpClient, what does the timeout request option cover, and how does its failure look to the subscriber?`timeout` is a number of milliseconds for the backend request itself; it does not include time spent in interceptors. When it expires the backend aborts the request and the subscriber receives an `HttpErrorResponse` with status 0. With the default `FetchBackend`, `error` is a `DOMException` named `TimeoutError`, which lets you tell a timeout apart from a network `TypeError`.
- Why can err.error be a string even though the request used the default JSON responseType?For a failed status, `HttpClient` tries to parse the error body as JSON and, if that fails, keeps the raw text instead of replacing it with a parse error. A proxy's HTML 502 page or a plain-text message therefore arrives as a string in `err.error`. Code that reads `err.error.message` must check the type first.
saying these in an interview costs you the question
- A 404 from HttpClient arrives in the next callback, so you must check response.ok.
- Status 0 means the server returned an empty response.
- A 200 response can never reach the error callback.
- HttpErrorResponse.error is always the parsed JSON error body.
- Status 0 tells you whether the cause was CORS, a timeout or the network.