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 fetch rejects with an error; ignore it instead of showing it to the user.

Code compare

React
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:

Angular
// 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`);  }}
Angular
// 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

ReactAngular
AbortController + effect cleanupAutomatic in httpResource()
Ignore errors when signal.abortedswitchMap cancels stale requests