In Angular, how should a template read an httpResource's value, error and statusCode so a 404 for an unknown product does not crash it?
answer
- value is not always safe to read
- guard before you read
- the error is an HttpErrorResponse
- status code survives the failure
basics
~20 sCheck error() or hasValue() before reading value(), because value() throws while the resource is in the error state. For a 404, error() holds the HttpErrorResponse and statusCode() returns 404, so the template can show a not-found message.
solid answer
~40 sAn `httpResource` that fails moves to the `error` status. In that state **`value()` throws** — Angular raises a `ResourceValueError` that wraps the cause — so a template that reads `value()` unguarded crashes the view. Guard it: branch on `error()` first, or use `hasValue()`, which returns `false` in the error state. For HTTP failures, `error()` holds the `HttpErrorResponse`, and the HTTP-specific signals are filled even on failure: `statusCode()` is `404` and `headers()` has the error response's headers. So a `computed(() => reviews.statusCode() === 404)` can drive a "product not found" message while other failures show a generic one. `isLoading()` is true in both `loading` (inputs changed) and `reloading` (`reload()`), and only `reloading` keeps the previous value on screen.
code
ts · 37 linesimport { Component, computed, input } from '@angular/core';
import { HttpErrorResponse, httpResource } from '@angular/common/http';
interface Review {
id: string;
rating: number;
text: string;
}
@Component({
selector: 'app-product-reviews',
template: `
@if (notFound()) {
<p>This product does not exist.</p>
} @else if (reviews.error()) {
<p>Reviews are unavailable ({{ failureStatus() }}).</p>
<button (click)="reviews.reload()">Retry</button>
} @else if (reviews.hasValue()) {
<p>{{ reviews.value().length }} reviews</p>
} @else if (reviews.isLoading()) {
<p>Loading reviews...</p>
}
`,
})
export class ProductReviews {
readonly productId = input.required<string>();
readonly reviews = httpResource<Review[]>(() => `/api/products/${this.productId()}/reviews`);
// statusCode() is filled for HttpErrorResponse failures too.
readonly notFound = computed(() => this.reviews.statusCode() === 404);
readonly failureStatus = computed(() => {
const error = this.reviews.error();
return error instanceof HttpErrorResponse ? `HTTP ${error.status}` : 'unexpected response';
});
}go deeper
Guard value() with error() or hasValue() in the template, and show a loading message with isLoading().
Explain the six statuses, why value() throws in error, the difference between loading and reloading, and how defaultValue changes hasValue().
Use statusCode(), headers() and HttpErrorResponse narrowing to give users precise states, and handle parse failures separately from HTTP failures.
Define a shared pattern for loading, empty, not-found and failure states across resource-backed views so the UX and error reporting stay uniform.
## The six statuses, seen from an HTTP call `status()` on an `httpResource` returns one of six strings. For a reviews panel keyed by `productId`: | Status | When it happens | `value()` | |---|---|---| | `idle` | The request function returned `undefined` (no product selected) | `defaultValue`, or `undefined` | | `loading` | A new request is in flight because `productId` changed | `defaultValue`, or `undefined` | | `reloading` | `reload()` refetches the same request | The **previous** value stays | | `resolved` | The response arrived and was parsed | The response body | | `error` | The request failed, or `parse` threw | **Throws** | | `local` | Code called `set()` or `update()` on the resource | The locally set value | `isLoading()` is simply `status() === 'loading' || status() === 'reloading'`. ## Why `value()` throws in the error state In the error state there is no value to return, and returning `undefined` silently would hide the failure. Angular therefore throws a `ResourceValueError` whose `cause` is the original error. The guide says it directly: reading `value` on a resource in an error state throws at runtime, and you should guard reads with `hasValue()`. The practical rules: 1. **Branch on `error()` before touching `value()`**, or wrap the success branch in `@if (reviews.hasValue())`. 2. `hasValue()` returns `false` in the error state instead of throwing, and `false` while the value is `undefined`. 3. A `computed` that reads `value()` inherits the problem: it throws too when read in the error state. ## What `error()` contains - For a failed HTTP call, `error()` is the **`HttpErrorResponse`** that `HttpClient` produced: `status`, `statusText`, `url`, the error body in `.error`. - For a response that `parse` rejected, it is the error the `parse` function threw. - The signal is typed `Error | undefined`, so narrow with `instanceof HttpErrorResponse` before reading HTTP fields. ## The HTTP-only signals `httpResource` adds three signals to the base resource: - **`statusCode()`** — the response status. It is set for successful responses **and for `HttpErrorResponse` failures**, so a 404 gives `404` and a network failure gives `0`. - **`headers()`** — the response headers, available when the status is `resolved` or `error`, `undefined` while loading. - **`progress()`** — the latest download progress event when the request set `reportProgress: true`. These reset when the request changes, so they always describe the current product. ## A panel that handles every case ```ts readonly notFound = computed(() => this.reviews.statusCode() === 404); ``` ```html @if (notFound()) { <p>This product does not exist.</p> } @else if (reviews.error()) { <p>Reviews are unavailable. <button (click)="reviews.reload()">Retry</button></p> } @else if (reviews.hasValue()) { <app-review-list [reviews]="reviews.value()" /> } @else if (reviews.isLoading()) { <p>Loading reviews...</p> } ``` Note the order: errors first, then values, then loading. During `reloading` the old list stays visible because `hasValue()` is still true; during `loading` for a different product, `hasValue()` is false (unless a `defaultValue` is set) and the loading message shows. ## Reading the state outside the template The same guard applies in TypeScript. A `computed` or `effect` that reads `value()` must check `error()` or `hasValue()` first. When code needs status and data together, `snapshot()` returns one object: `{ status: 'error', error }` in the error state, or `{ status, value }` otherwise, so a `switch` on `snapshot().status` can never read a value that is not there. ## `defaultValue` changes the picture With `{ defaultValue: [] }`, the value type loses `undefined` and `value()` returns `[]` while idle or loading. That simplifies `@for` loops, but `hasValue()` is then `true` during loading, so use `isLoading()` if the UI must distinguish "loading" from "no reviews". It does not change the error rule: `value()` still throws in the error state. ## Common mistakes - Reading `reviews.value().length` in the template without a guard. - Treating `status() === 'loading'` as the only loading state and missing `reloading`. - Using `error()?.status` without narrowing, which does not type-check on `Error`. - Assuming `statusCode()` is only set for successful responses.
- Why does the reviews list stay visible after clicking Retry but disappear when the product changes?`reload()` refetches the same request and moves the resource to `reloading`, which keeps the previous value, so `hasValue()` stays true. A new `productId` changes the request itself, which moves the resource to `loading` and resets the value to the default or `undefined`, because the old product's reviews are not a valid value for the new product.
- What does error() hold when the parse function of an httpResource throws, and how does statusCode() look then?The HTTP call succeeded, so `statusCode()` shows the real status, typically 200, and `headers()` has the response headers. The resource still moves to `error`, and `error()` holds whatever `parse` threw. Code that branches on `instanceof HttpErrorResponse` must therefore treat parse failures as a separate case.
saying these in an interview costs you the question
- value() returns undefined when an httpResource is in the error state.
- hasValue() throws in the error state, just like value().
- statusCode() is only set when the response was successful.
- isLoading() is false during reload(), because a value is already present.
- The previous product's reviews stay in value() while the next product loads.