AbortController in real apps: Cancel requests on route change
Author
Md Arshad Khan
Date Published

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;34const abortEventListener=(event)=>{5 console.log(signal.aborted); // true6 console.log(signal.reason) // Hello Signals7}89signal.addEventListener("abort", abortEventListener);10controller.abort("Hello Signals"); // aborting message11signal.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';23interface User {4 id: number;5 name: string;6 email: string;7}89function UserProfile({ userId }: { userId: string }) {10 const [user, setUser] = useState<User | null>(null);11 const [error, setError] = useState<string>('');1213 useEffect(() => {14 const controller = new AbortController();1516 fetch(`/api/users/${userId}`, {17 signal: controller.signal18 })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 });2829 return () => controller.abort();30 }, [userId]);3132 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';23function StockSearchAutocomplete() {4 const [query, setQuery] = useState('');5 const [results, setResults] = useState<string[]>([]);67 useEffect(() => {8 if (!query.trim()) {9 setResults([]);10 return;11 }1213 const controller = new AbortController();14 const timeoutId = setTimeout(() => {15 fetch(`/api/stocks/search?q=${query}`, {16 signal: controller.signal17 })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);2627 return () => {28 clearTimeout(timeoutId);29 controller.abort();30 };31 }, [query]);3233 return (34 <input35 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>();34 async fetch<T>(5 key: string,6 url: string,7 options?: RequestInit8 ): Promise<T> {9 this.cancel(key);1011 const controller = new AbortController();12 this.controllers.set(key, controller);1314 try {15 const response = await fetch(url, {16 ...options,17 signal: controller.signal18 });1920 if (!response.ok) throw new Error('Request failed');21 return response.json();22 } finally {23 this.controllers.delete(key);24 }25 }2627 cancel(key: string) {28 const controller = this.controllers.get(key);29 if (controller) {30 controller.abort();31 this.controllers.delete(key);32 }33 }3435 cancelAll() {36 for (const controller of this.controllers.values()) {37 controller.abort();38 }39 this.controllers.clear();40 }41}4243const requestManager = new RequestManager();4445function useMarketData(symbol: string) {46 const [data, setData] = useState(null);4748 useEffect(() => {49 requestManager50 .fetch(`market-${symbol}`, `/api/market/${symbol}`)51 .then(setData);5253 return () => requestManager.cancel(`market-${symbol}`);54 }, [symbol]);5556 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);34 useEffect(() => {5 const controller = new AbortController();67 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 });2122 return () => controller.abort();23 }, []);2425 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