How can I handle fetch request cancellation and timeouts gracefully in an Expo app using AbortController?
0 reputation · 25 Nov 2021, 17:39 UTC
0 reputation · 25 Nov 2021, 17:39 UTC
In Expo, when fetching data, we need to cancel requests if the component unmounts or if a timeout occurs to avoid memory leaks and stale state. The fetch API supports AbortController, but integrating it with React Native's lifecycle and setting a timeout introduces uncertainty about proper cleanup and race conditions.
Constraints include ensuring that the abort signal is passed to fetch, that timeout logic does not interfere with normal completion, and that cleanup functions run only once. Uncertainty remains about the best way to combine AbortController with setTimeout or useEffect to guarantee that either a successful response, a timeout, or a cancellation leads to predictable state updates.
How should I create and attach an AbortSignal to a fetch request in an Expo component so that it aborts on unmount? What is the recommended pattern to implement a timeout that aborts the fetch after a given interval without causing duplicate abort calls? How can I handle the AbortError in the catch block to differentiate it from other network errors?
26525 reputation · 26 Nov 2021, 03:30 UTC
In a functional Expo component you can create a single AbortController per request, pass its signal to fetch, start a setTimeout that calls controller.abort(), and clear that timer in the finally block or the effect cleanup. The fetch promise will reject with an AbortError if the request is aborted, allowing you to distinguish it from other network errors.
finally block ensures the timeout is cleared regardless of success, failure, or abort.abort(), the request can’t be aborted twice.AbortError.name === 'AbortError' lets you skip state updates when the component is gone.function useCancelableFetch(url, options = {}, timeoutMs = 0) {
const controller = new AbortController();
const signal = controller.signal;
let timer;
const fetchPromise = fetch(url, { ...options, signal })
.then(async res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
});
if (timeoutMs > 0) {
timer = setTimeout(() => controller.abort(), timeoutMs);
}
const cancel = () => {
controller.abort();
if (timer) clearTimeout(timer);
};
const wrapped = fetchPromise
.finally(() => {
if (timer) clearTimeout(timer);
});
return [wrapped, cancel];
}
import React, { useEffect, useState, useRef } from 'react';
import { Text, ActivityIndicator, View } from 'react-native';
export default function DataScreen() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
const cancelRef = useRef(null);
useEffect(() => {
const [promise, cancel] = useCancelableFetch(
'https://api.example.com/items',
{},
5000 // 5‑second timeout
);
cancelRef.current = cancel;
promise
.then(setData)
.catch(err => {
if (err.name === 'AbortError') {
console.log('Request aborted');
} else {
setError(err.message);
}
})
.finally(() => setLoading(false));
return () => {
// Component unmount: abort any in‑flight request
cancelRef.current?.();
};
}, []);
if (loading) return ;
if (error) return {error};
return {/* render data */};
}
cancelRef.current() (e.g., with a button) – the promise rejects immediately with AbortError.Because finally runs after both success and failure, the timer is always cleared. The controller is only aborted once: either by the timer or by component unmount. If the server ignores the abort signal, the underlying TCP connection may still linger until the OS times out, but the UI will no longer update because the component has been removed.
AbortController (SDK 48+ is safe).finally or the cleanup function to avoid memory leaks.err.name === 'AbortError' to distinguish aborts from network or HTTP errors.If you experience unexpected aborts on a particular endpoint, check whether the server supports the AbortSignal header or whether the request is being retried by Expo’s networking layer. Provide the endpoint and any retry logic you have in place so we can refine the solution.
Use comments to ask for clarification. Post a solution as an answer.
26,525 reputation · 25 Nov 2021, 21:55 UTC
While the provided pattern is effective for modern environments, it is important to verify the JavaScript engine version used by your Expo project. In some older React Native versions or specific legacy environments, AbortController may not be available globally.
If you encounter a ReferenceError: AbortController is not defined, you will need to include a polyfill. A common approach is installing abort-controller and initializing it at the entry point of your app:
import 'abort-controller-polyfill/dist/abort-controller-polyfill-only';
One critical distinction to keep in mind: calling controller.abort() terminates the client-side connection and prevents the fetch promise from resolving. However, it does not guarantee that the server stops processing the request. If the request has already reached the backend, the server may still complete the operation despite the client disconnecting.