skip to content

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?

level: middleimportance: should knowfreq 45%

answer

  1. value is not always safe to read
  2. guard before you read
  3. the error is an HttpErrorResponse
  4. status code survives the failure

basics

~20 s

Check 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 s

An `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 lines
ts
import { 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

for a junior

Guard value() with error() or hasValue() in the template, and show a loading message with isLoading().

for a middle

Explain the six statuses, why value() throws in error, the difference between loading and reloading, and how defaultValue changes hasValue().

for a senior

Use statusCode(), headers() and HttpErrorResponse narrowing to give users precise states, and handle parse failures separately from HTTP failures.

for a principal

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.