Sending HTTP Requests (e.g. Connecting to a Database)Entry 3 of 7

Loading and Error States

Tracking whether a request is in progress or has failed, so the UI can show a spinner or an error message.

Notes

  • A request has three outcomes to render: loading, error, and data. Each needs its own state.
  • fetch only rejects on network failures. A 404 or 500 still resolves, so check response.ok and throw your own error.
  • Check response.ok before parsing: error responses may not contain valid JSON.
  • In TypeScript, a caught error has type unknown. Narrow it with err instanceof Error before reading .message.
  • Use finally to turn loading off whether the request succeeded or failed.

What / Why / When

What

isFetching and error state alongside the data.

Why

Users need feedback; silent failures look like empty data.

When

Every request whose result is shown in the UI.

Use case

Show "Fetching places…" while loading and an error panel if the server fails.

Code compare

React
import { useEffect, useState } from "react";​type Place = { id: string; title: string };​export function AvailablePlaces() {  const [availablePlaces, setAvailablePlaces] = useState<Place[]>([]);  const [isFetching, setIsFetching] = useState(true);  const [error, setError] = useState<string | null>(null);​  useEffect(() => {    async function fetchPlaces() {      try {        const response = await fetch("http://localhost:3000/places");​        if (!response.ok) {          throw new Error("Failed to fetch places.");        }​        const resData: { places: Place[] } = await response.json();        setAvailablePlaces(resData.places);      } catch (err) {        setError(err instanceof Error ? err.message : "Could not fetch places.");      } finally {        setIsFetching(false);      }    }​    fetchPlaces();  }, []);​  if (error) return <p className="error">An error occurred: {error}</p>;  if (isFetching) return <p>Fetching place data...</p>;​  return (    <ul>      {availablePlaces.map((place) => (        <li key={place.id}>{place.title}</li>      ))}    </ul>  );}

The resource returned by PlacesService.loadAvailablePlaces() (Entry 2) tracks loading and error state for you. HttpClient also treats non-2xx responses as errors automatically, so there's no response.ok check.

Angular
import { Component, computed, inject } from "@angular/core";import { PlacesService } from "./places.service";​@Component({  selector: "app-available-places",  template: `    @if (placesResource.isLoading()) {      <p>Fetching place data...</p>    } @else if (placesResource.error()) {      <p class="error">An error occurred: failed to fetch places.</p>    } @else {      <ul>        @for (place of availablePlaces(); track place.id) {          <li>{{ place.title }}</li>        }      </ul>    }  `,})export class AvailablePlacesComponent {  protected placesResource = inject(PlacesService).loadAvailablePlaces();​  availablePlaces = computed(() =>    this.placesResource.hasValue() ? this.placesResource.value().places : [],  );}

Differences and gotchas

  • React needs three useState calls and a try/catch/finally. httpResource() provides isLoading(), error(), and value() out of the box.
  • fetch resolves on HTTP errors; HttpClient rejects on them. This is the most common bug when moving between the two.
  • Start isFetching as true in React. Starting it as false briefly renders an empty list before the effect runs.

Quick reference

ReactAngular
useState for loading, error, datahttpResource() signals
if (!response.ok) throw ...Automatic for non-2xx responses
try / catch / finallyerror callback or resource.error()