Md Arshad Khan | Full Stack Engineer Logo
Javascript,  React,  frontend

AbortController in real apps: Cancel requests on route change

Author

Md Arshad Khan

Date Published

AbortController in real apps blog hero image

Introduction to AbortController API

Let's create an instance od AbortController API

1const controller = new AbortController();

Instance of AbortController class exports abort method and signal property. Invoking abort emits the abort event to notify the abortable API.

You can also pass a reason for aborting the abort method. If not provided a reason it defaults to AbortError.

To listen for the abort event , you need to add an event listener to the controller's signal property using addEventListener.

Similarly to remove the eventListener you can use removeEventListener method.

1const controller=new AbortController();
2const {signal}=controller;
3
4const abortEventListener=(event)=>{
5 console.log(signal.aborted); // true
6 console.log(signal.reason) // Hello Signals
7}
8
9signal.addEventListener("abort", abortEventListener);
10controller.abort("Hello Signals"); // aborting message
11signal.removeEventListener("abort",abortEventListener);

Why Request Cancellation Matters

Performance Impact


When users type in a search box, every keystroke can trigger an API call. Without cancellation, you’re making dozens of unnecessary requests that compete for bandwidth and server resources. In data-heavy dashboards where real-time data flows constantly, this compounds quickly.

Preventing Race Conditions

The response from your first API call might arrive after your second one, displaying stale data. This is catastrophic in apps where showing outdated prices or balances can lead to bad decisions.

Memory Leak Prevention


When components unmount but their fetch requests continue, trying to update state on unmounted components causes memory leaks and console errors. AbortController cleans up these dangling requests automatically.

Basic Implementation in React + TypeScript

1import { useEffect, useState } from 'react';
2
3interface User {
4 id: number;
5 name: string;
6 email: string;
7}
8
9function UserProfile({ userId }: { userId: string }) {
10 const [user, setUser] = useState<User | null>(null);
11 const [error, setError] = useState<string>('');
12
13 useEffect(() => {
14 const controller = new AbortController();
15
16 fetch(`/api/users/${userId}`, {
17 signal: controller.signal
18 })
19 .then(res => res.json())
20 .then(data => setUser(data))
21 .catch(err => {
22 if (err.name === 'AbortError') {
23 console.log('Request cancelled');
24 } else {
25 setError(err.message);
26 }
27 });
28
29 return () => controller.abort();
30 }, [userId]);
31
32 return user ? <div>{user.name}</div> : null;
33}
34

The cleanup function in useEffect cancels the request when the component unmounts or userId changes, preventing state updates on stale requests.

Real-World Pattern: Search Autocomplete

1import { useEffect, useState, useCallback } from 'react';
2
3function StockSearchAutocomplete() {
4 const [query, setQuery] = useState('');
5 const [results, setResults] = useState<string[]>([]);
6
7 useEffect(() => {
8 if (!query.trim()) {
9 setResults([]);
10 return;
11 }
12
13 const controller = new AbortController();
14 const timeoutId = setTimeout(() => {
15 fetch(`/api/stocks/search?q=${query}`, {
16 signal: controller.signal
17 })
18 .then(res => res.json())
19 .then(data => setResults(data))
20 .catch(err => {
21 if (err.name !== 'AbortError') {
22 console.error('Search failed:', err);
23 }
24 });
25 }, 300);
26
27 return () => {
28 clearTimeout(timeoutId);
29 controller.abort();
30 };
31 }, [query]);
32
33 return (
34 <input
35 value={query}
36 onChange={e => setQuery(e.target.value)}
37 placeholder="Search stocks..."
38 />
39 );
40}
41

This combines debouncing with cancellation. Each keystroke cancels the previous request and timer, ensuring only the final search executes.

Advanced Pattern: Request Manager for Data-Heavy Apps

For dashboards with multiple concurrent data streams, a centralized request manager prevents chaos:

1class RequestManager {
2 private controllers = new Map<string, AbortController>();
3
4 async fetch<T>(
5 key: string,
6 url: string,
7 options?: RequestInit
8 ): Promise<T> {
9 this.cancel(key);
10
11 const controller = new AbortController();
12 this.controllers.set(key, controller);
13
14 try {
15 const response = await fetch(url, {
16 ...options,
17 signal: controller.signal
18 });
19
20 if (!response.ok) throw new Error('Request failed');
21 return response.json();
22 } finally {
23 this.controllers.delete(key);
24 }
25 }
26
27 cancel(key: string) {
28 const controller = this.controllers.get(key);
29 if (controller) {
30 controller.abort();
31 this.controllers.delete(key);
32 }
33 }
34
35 cancelAll() {
36 for (const controller of this.controllers.values()) {
37 controller.abort();
38 }
39 this.controllers.clear();
40 }
41}
42
43const requestManager = new RequestManager();
44
45function useMarketData(symbol: string) {
46 const [data, setData] = useState(null);
47
48 useEffect(() => {
49 requestManager
50 .fetch(`market-${symbol}`, `/api/market/${symbol}`)
51 .then(setData);
52
53 return () => requestManager.cancel(`market-${symbol}`);
54 }, [symbol]);
55
56 return data;
57}
58

This pattern lets you manage requests by key, cancel specific ones, or abort everything when navigating away.

Cancelling Multiple Requests at Once

When loading a dashboard with multiple data sources, use a single controller for all requests:

1function DashboardLoader() {
2 const [data, setData] = useState(null);
3
4 useEffect(() => {
5 const controller = new AbortController();
6
7 Promise.all([
8 fetch('/api/portfolio', { signal: controller.signal }),
9 fetch('/api/watchlist', { signal: controller.signal }),
10 fetch('/api/alerts', { signal: controller.signal })
11 ])
12 .then(responses => Promise.all(responses.map(r => r.json())))
13 .then(([portfolio, watchlist, alerts]) => {
14 setData({ portfolio, watchlist, alerts });
15 })
16 .catch(err => {
17 if (err.name === 'AbortError') {
18 console.log('Dashboard load cancelled');
19 }
20 });
21
22 return () => controller.abort();
23 }, []);
24
25 return data ? <Dashboard {...data} /> : <Loader />;
26}
27

One abort() call cancels all three requests instantly, preventing partial data states.

Key Takeaways

1. Always return a cleanup function from useEffect that calls controller.abort()

2. Check for err.name === ‘AbortError’ to distinguish cancellation from real errors

3. Combine debouncing with cancellation for search/autocomplete

4. Use a request manager pattern for complex apps with many concurrent requests

5. One AbortController can cancel multiple fetch calls simultaneously

AbortController isn’t just about preventing bugs. It’s about respecting your users’ bandwidth, your servers’ resources, and building apps that feel snappy and intentional.


Also Checkout

1. The complete guide to the AbortController API - LogRocket Blog

2. Learn AbortController with Examples - Never to look back again - DEV Community

3. Understanding AbortController in Node.js: A Complete Guide | Better Stack Community