skip to content

In React Native, fetch() has no timeout option; how do you stop a request hanging on a bad mobile network, and what does aborting cancel?

level: middleimportance: should knowfreq 48%

answer

  1. no timeout key in the fetch options
  2. AbortController plus a timer
  3. clearTimeout in finally
  4. AbortSignal.timeout in 0.87
  5. abort cancels the native request

basics

~20 s

Pass an AbortController's signal to fetch and call abort() from a setTimeout, clearing the timer in finally; React Native 0.87 also has AbortSignal.timeout(ms). Aborting rejects the promise with an abort error and cancels the underlying native HTTP request.

solid answer

~40 s

`fetch` has no `timeout` option, and you should not rely on a platform default: React Native's Android OkHttp client is built with connect, read and write timeouts of `0`, meaning no limit. So I create an `AbortController`, pass `controller.signal` to `fetch`, start a `setTimeout` that calls `controller.abort()`, and clear the timer in `finally` so it cannot fire later. In React Native 0.87, `AbortSignal.timeout(ms)` and `AbortSignal.any([...])` are built in, which lets me combine a timeout with a caller's cancel signal. When the signal fires, the promise rejects with an abort error and the native request is cancelled, not just ignored. Libraries built on `XMLHttpRequest`, such as axios, expose a `timeout` setting instead, which is passed down to the native request.

code

typescript · 13 lines
typescript
export async function getJson<T>(url: string, ms = 10_000): Promise<T> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  try {
    const res = await fetch(url, { signal: controller.signal });
    if (!res.ok) {
      throw new Error(`HTTP ${res.status}`);
    }
    return (await res.json()) as T;
  } finally {
    clearTimeout(timer);
  }
}

go deeper

for a junior

Recall that fetch has no timeout option and that an AbortController signal plus a timer is how you give up on a request.

for a middle

Explain the full pattern with clearTimeout in finally, AbortSignal.timeout and any on React Native 0.87, and that abort cancels the native request underneath.

for a senior

Put timeouts in one request layer with per-call deadlines, distinguish user cancellation from timeouts, and pair timeouts with a retry policy that respects idempotency.

for a principal

Set a network budget for the app: deadlines per request class, retry and backoff rules, and what the UI promises users on slow networks, enforced by one shared client.

## Why a timeout is your job The Fetch API has no `timeout` field in its options object, and mobile networks fail in ways desktop networks rarely do: a train enters a tunnel, the phone hops from Wi-Fi to cellular, a captive portal swallows packets. A request can then sit **pending** with a spinner on screen. Do not count on a platform default to rescue you. React Native's Android networking builds its OkHttp client with **connect, read and write timeouts of `0`, which OkHttp treats as no limit**. React Native's `XMLHttpRequest` also starts with `timeout = 0`. The only bound you can trust is the one you set. ## The standard pattern 1. Create an `AbortController` per request. 2. Pass `controller.signal` as the `signal` option of `fetch`. 3. Start a timer that calls `controller.abort()`. 4. Clear the timer in `finally`, whether the request succeeded, failed or was aborted. 5. Keep the body read (`response.json()`) inside the same `try` if you want the deadline to cover the whole exchange. In **React Native 0.87**, the built-in `AbortSignal` also implements `AbortSignal.timeout(ms)`, which aborts itself with a `TimeoutError` reason, and `AbortSignal.any(signals)`, which fires when any input signal fires. Together they express "abort on timeout **or** when the user leaves the screen" without hand-wiring listeners. ## What abort actually does It helps to know the layers: - **React Native's built-in `fetch`** is a polyfill over React Native's `XMLHttpRequest`. Aborting calls `xhr.abort()`, which asks the native networking module to **cancel the request** (`abortRequest`). The `NSURLSession` task or OkHttp call is torn down, so bytes stop flowing and the socket is released. - **`expo/fetch`**, the global `fetch` in Expo SDK 56 and later, also listens for the signal, errors the response body stream and cancels its native request. - The `fetch` promise **rejects**. Code that shows an error message should distinguish a deliberate cancel (user left the screen) from a timeout; the rejection's shape differs between implementations, so decide from `signal.reason` or your own flag rather than parsing messages. Aborting stops the client. It does **not** undo work the server has already started, which is why non-idempotent calls need their own retry rules. ## Choosing where to put the logic | Option | Timeout mechanism | Notes | |---|---|---| | Built-in `fetch` | `AbortController` + timer, or `AbortSignal.timeout` | No dependency; you write the wrapper once | | `XMLHttpRequest` | `xhr.timeout` (ms) plus `ontimeout` | Value is passed to the native request | | axios | `timeout` in the request or instance config | Runs on React Native's XHR; also accepts a `signal` | Whichever you choose, put it in **one request helper** so every screen gets the same deadline, and pick values per call type: a search-as-you-type request can give up quickly, while a large upload needs far longer. ## Cancelling when the user leaves a screen Timeouts cover a slow network; cancellation covers a user who has moved on. On a React Native screen the usual shape is to create a controller when the screen starts loading, pass its signal to the request helper, and call `abort()` in the effect's cleanup. Combined with a deadline through `AbortSignal.any`, one signal then means "stop if the user leaves **or** time runs out". This matters more on mobile than on the web: a stack navigator keeps previous screens mounted, while a user who swipes back from a detail screen unmounts it. Without cancellation, a slow response lands after the screen is gone, the state update is wasted, and on a metered cellular connection the user paid for bytes nobody will see. ## Common mistakes - Forgetting `clearTimeout`, so a timer from a finished request aborts nothing but keeps a closure alive, or, worse, a shared controller aborts the next request. - Reusing one `AbortController` for several requests; once aborted, a signal stays aborted forever. - Treating every abort as a network error and showing a red banner when the user simply navigated away. - Assuming the operating system will end a stuck request quickly on its own. In an interview, name the missing option, show the controller-plus-timer pattern, mention `AbortSignal.timeout` on current React Native, and explain that abort cancels the native request but not server-side effects.

  • How do you tell a timeout from the user navigating away when the fetch promise rejects?
    Inspect why the signal fired rather than the error text. `AbortSignal.timeout` aborts with a `TimeoutError` reason, while your own `controller.abort()` uses the default reason or one you pass. Check `signal.reason` (or a flag you set) and show an error only for timeouts; silently ignore user cancellations.
  • After a timeout you want to retry. Which requests is that safe for?
    Reads and idempotent writes can be retried, ideally with exponential backoff and jitter and a small attempt cap. For a write that may have reached the server, retry only if the API supports an idempotency key; otherwise the abort may have left the operation half-done.
  • How does axios expose a timeout in a React Native app?
    axios accepts a `timeout` value in milliseconds in the instance or request config and rejects when it elapses. In React Native it runs on the built-in `XMLHttpRequest`, whose `timeout` is passed to the native request, and it also accepts an `AbortController` `signal` for cancellation.

saying these in an interview costs you the question

  • fetch in React Native accepts a timeout option in milliseconds.
  • The native client times out stuck requests after a short default anyway.
  • Aborting only makes the promise reject; the native request keeps downloading.
  • One AbortController can be shared and reused across many requests.
  • Aborting a POST guarantees the server did not process it.