Sending HTTP Requests (e.g. Connecting to a Database)Entry 6 of 7
Race Conditions and Cancelling Requests
A race condition happens when an older, slower request finishes after a newer one and overwrites its result.
Notes
- If an effect fetches based on a changing value (e.g.
teamId), switching quickly can send several requests. They may resolve out of order. - Cancel the previous request in the effect's cleanup with an
AbortController. The cleanup runs before the effect re-runs and when the component unmounts. - An aborted
fetchrejects with an error; ignore it instead of showing it to the user.
Code compare
import { useEffect, useState } from "react";type Player = { id: string; name: string };export function TeamPlayers({ teamId }: { teamId: string }) { const [players, setPlayers] = useState<Player[]>([]); const [error, setError] = useState<string | null>(null); useEffect(() => { const controller = new AbortController(); async function fetchPlayers() { try { const res = await fetch(`/api/teams/${teamId}/players`, { signal: controller.signal }); if (!res.ok) throw new Error("Failed to load players."); setPlayers(await res.json()); } catch (err) { if (controller.signal.aborted) return; // cancelled on purpose: not an error setError(err instanceof Error ? err.message : "Unknown error"); } } fetchPlayers(); return () => controller.abort(); // cancel when teamId changes or on unmount }, [teamId]); if (error) return <p>Error: {error}</p>; return ( <ul> {players.map((p) => ( <li key={p.id}>{p.name}</li> ))} </ul> );}The service exposes a method that takes the team ID as a signal. httpResource() re-fetches when that signal changes and cancels the previous request automatically:
// players.service.tsimport { Injectable, Signal } from "@angular/core";import { httpResource } from "@angular/common/http";export type Player = { id: string; name: string };@Injectable({ providedIn: "root" })export class PlayersService { loadTeamPlayers(teamId: Signal<string>) { return httpResource<Player[]>(() => `/api/teams/${teamId()}/players`); }}// team-players.component.tsimport { Component, inject, input } from "@angular/core";import { PlayersService } from "./players.service";@Component({ selector: "app-team-players", template: ` @if (playersResource.hasValue()) { <ul> @for (p of playersResource.value(); track p.id) { <li>{{ p.name }}</li> } </ul> } `,})export class TeamPlayersComponent { teamId = input.required<string>(); protected playersResource = inject(PlayersService).loadTeamPlayers(this.teamId);}With observables, switchMap gives the same behavior: each new value cancels the previous inner request.
Quick reference
| React | Angular |
|---|---|
AbortController + effect cleanup | Automatic in httpResource() |
Ignore errors when signal.aborted | switchMap cancels stale requests |