menu

Questions & Answers

Fetch API request timeout?

I have a fetch-api POST request:

fetch(url, {
  method: 'POST',
  body: formData,
  credentials: 'include'
})

I want to know what is the default timeout for this? and how can we set it to a particular value like 3 seconds or indefinite seconds?

Answers(14) :

Here's a SSCCE using NodeJS which will timeout after 1000ms:

import fetch from 'node-fetch';

const controller = new AbortController();
const timeout = setTimeout(() => {
    controller.abort();
}, 1000); // will time out after 1000ms

fetch('https://www.yourexample.com', {
    signal: controller.signal,
    method: 'POST',
    body: formData,
    credentials: 'include'
}
)
.then(response => response.json())
.then(json => console.log(json))
.catch(err => {
    if(err.name === 'AbortError') {
        console.log('Timed out');
    }}
)
.finally( () => {
    clearTimeout(timeout);
});

Building on Endless' excellent answer, I created a helpful utility function.

const fetchTimeout = (url, ms, { signal, ...options } = {}) => {
    const controller = new AbortController();
    const promise = fetch(url, { signal: controller.signal, ...options });
    if (signal) signal.addEventListener("abort", () => controller.abort());
    const timeout = setTimeout(() => controller.abort(), ms);
    return promise.finally(() => clearTimeout(timeout));
};
  1. If the timeout is reached before the resource is fetched then the fetch is aborted.
  2. If the resource is fetched before the timeout is reached then the timeout is cleared.
  3. If the input signal is aborted then the fetch is aborted and the timeout is cleared.
const controller = new AbortController();

document.querySelector("button.cancel").addEventListener("click", () => controller.abort());

fetchTimeout("example.json", 5000, { signal: controller.signal })
    .then(response => response.json())
    .then(console.log)
    .catch(error => {
        if (error.name === "AbortError") {
            // fetch aborted either due to timeout or due to user clicking the cancel button
        } else {
            // network error or json parsing error
        }
    });
Comments:
2023-01-17 00:47:08
This is fantastic! It covers all the nasty edge cases that were problematic in other answers, and you provide a clear usage example.
2023-01-17 00:47:08
What happens if, in your example, the firstly created const controller = new AbortController() is used after fetchTimeout finished its execution? My concern is based on the event listener that binds this first controller to the controller created inside the function (for which, I assume, the lifetime is tied with the function execution).
2023-01-17 00:47:08
@vdavid I'm not sure I follow what you mean. Can you give me a code example with comments describing your problem?

Using a promise race solution will leave the request hanging and still consume bandwidth in the background and lower the max allowed concurrent request being made while it's still in process.

Instead use the AbortController to actually abort the request, Here is an example

const controller = new AbortController()

// 5 second timeout:

const timeoutId = setTimeout(() => controller.abort(), 5000)

fetch(url, { signal: controller.signal }).then(response => {
  // completed request before timeout fired

  // If you only wanted to timeout the request, not the response, add:
  // clearTimeout(timeoutId)
})

Alternative you can use the newly added AbortSignal.timeout(5000)... but it is not well implemented in most browser right now. All green env have this now. You will lose control over manually closing the request. Both upload and download will have to finish within a total time of 5s

// a polyfill for it would be:
AbortSignal.timeout ??= function timeout(ms) {
  const ctrl = new AbortController()
  setTimeout(() => ctrl.close(), ms)
  return ctrl.signal
}


fetch(url, { signal: AbortSignal.timeout(5000) })

AbortController can be used for other things as well, not only fetch but for readable/writable streams as well. More newer functions (specially promise based ones) will use this more and more. NodeJS have also implemented AbortController into its streams/filesystem as well. I know web bluetooth are looking into it also. Now it can also be used with addEventListener option and have it stop listening when the signal ends

Comments:
2023-01-17 00:47:08
This looks even better than the promise-race-solution because it probably aborts the request instead of just taking the earlier response. Correct me if I'm wrong.
2023-01-17 00:47:08
The answer doesn't explain what AbortController is. Also, it is experimental and needs to be polyfilled in unsupported engines, also it's not a syntax.
2023-01-17 00:47:08
It might not explain what AbortController is (I added a link to the answer to make it easier for the lazy ones), but this is the best answer so far, as it highlights the fact that merely ignoring a request doesn't mean it's still not pending. Great answer.
2023-01-17 00:47:08
"I added a link to the answer to make it easier for the lazy ones" -- it should really come with a link and more information as per the rules tbh. But thank you for improving the answer.
2023-01-17 00:47:08
Better to have this answer than no answer because people are put-off by nitpickery, tbh
2023-01-17 00:47:08
This works great for me except for when I go to run my tests with jest. Then for some reason it can't find AbortController. Does anyone have a resolution?
2023-01-17 00:47:08
@EstusFlask currently it is wide supported
2023-01-17 00:47:08
To catch the timeout I found the following code extremely helpful, courtesy of the original Google engineer I think, might be useful for beginners in this: developers.google.com/web/updates/2017/09/abortable-fetch - fetch(url, { signal }).then(response => { return response.text(); }).then(text => { console.log(text); }).catch(err => { if (err.name === 'AbortError') { console.log('Fetch aborted'); } else { console.error('Uh oh, an error!', err); } });
2023-01-17 00:47:08
How do you clearTimeout for AbortSignal.timeout(...)s? I want to set a timeout just for connection establishment (await fetch(...)), not body consumption (await response.arrayBuffer()).
2023-01-17 00:47:08
@КонстантинВан you can't clear the timeout for AbortSignal.timeout you have to use AbortController for manually canceling
2023-01-17 00:47:08
@Endless Does it mean that AbortSignal.timeout does not automatically clear timeouts (if internally setTimeout() is used)? So if I set a high AbortSignal.timeout like 60 sec. while having an avg response time of 50 ms (20 responses per sec). This would make it possible that a bot which opens every 50ms a new request could open 1200 timeouts running at the same time until the first timeout runs out of its 60 sec while with manually cleared timeouts there would be only 1 or maybe 2 timeouts at the same time.
2023-01-17 00:47:08
@bluefire, you are making me confused... but i guess you are correct. AbortSignal.timeout creates a own internal setTimeout(fn, 60000) and there is no clearTimeout function that can stop that timer from going off. And you can reuse this timeout signal instance for as many times as you want within 60 sec to multiple fetch requests. after 60s you would have to create a new signal

Update since my original answer is a bit outdated I recommend using abort controller like implemented here: https://stackoverflow.com/a/57888548/1059828 or take a look at this really good post explaining abort controller with fetch: How do I cancel an HTTP fetch() request?

outdated original answer:

I really like the clean approach from this gist using Promise.race

fetchWithTimeout.js

export default function (url, options, timeout = 7000) {
    return Promise.race([
        fetch(url, options),
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error('timeout')), timeout)
        )
    ]);
}

main.js

import fetch from './fetchWithTimeout'

// call as usual or with timeout as 3rd argument

// throw after max 5 seconds timeout error
fetch('http://google.com', options, 5000) 
.then((result) => {
    // handle result
})
.catch((e) => {
    // handle errors and timeout error
})
Comments:
2023-01-17 00:47:08
This causes an "Unhandled rejection" if a fetch error happens after timeout. This can be solved by handling (.catch) the fetch failure and rethrowing if the timeout hasn't happened yet.
2023-01-17 00:47:08
IMHO this could be improved futher with AbortController when rejecting, see stackoverflow.com/a/47250621.
2023-01-17 00:47:08
It would be better to clear the timeout if fetch is successful as well.
2023-01-17 00:47:08
It is a good approach but not very effective. The timeout should be cleared as Bob said, otherwise the program will wait until the timeout, even in the successful case
2023-01-17 00:47:08
For reference, AbortController is not available server side
2023-01-17 00:47:08
@jfunk that's not true. AbortController is available in Nodejs v15 and above.

there's no timeout support in the fetch API yet. But it could be achieved by wrapping it in a promise.

for eg.

  function fetchWrapper(url, options, timeout) {
    return new Promise((resolve, reject) => {
      fetch(url, options).then(resolve, reject);

      if (timeout) {
        const e = new Error("Connection timed out");
        setTimeout(reject, timeout, e);
      }
    });
  }
Comments:
2023-01-17 00:47:08
i like this one better, less repetitive to use more than once.
2023-01-17 00:47:08
The request is not canceled after the timeout here, correct? This may be fine for the OP, but sometimes you want to cancel a request client-side.
2023-01-17 00:47:08
@trysis well, yes. Recently implemented a solution for abort fetch with AbortController, but still experimental with limited browser support. Discussion
2023-01-17 00:47:08
That's funny, IE & Edge are the only ones that support it! Unless the mobile Mozilla site is acting up again...
2023-01-17 00:47:08
Firefox has been supporting it since 57. ::watching at Chrome::
2023-01-17 00:47:08
@FranklinYu Chrome supported it a few months later than when you added this comment :) Currently AbortController is widely supported (by ~all browsers). developer.mozilla.org/en-US/docs/Web/API/AbortController

If you haven't configured timeout in your code, It will be the default request timeout of your browser.

1) Firefox - 90 seconds

Type about:config in Firefox URL field. Find the value corresponding to key network.http.connection-timeout

2) Chrome - 300 seconds

Source

A more clean way to do it is actually in MDN: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal#aborting_a_fetch_operation_with_a_timeout

try {
    await fetch(url, { signal: AbortSignal.timeout(5000) });
} catch (e) {
    if (e.name === "TimeoutError") {
        console.log('5000 ms timeout');
    }
}

Comments:
2023-01-17 00:47:08
very nice - seems implemented for now only in Firefox. (See: developer.mozilla.org/en-US/docs/Web/API/AbortSignal/…)

Edit 1

As pointed out in comments, the code in the original answer keeps running the timer even after the promise is resolved/rejected.

The code below fixes that issue.

function timeout(ms, promise) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error('TIMEOUT'))
    }, ms)

    promise
      .then(value => {
        clearTimeout(timer)
        resolve(value)
      })
      .catch(reason => {
        clearTimeout(timer)
        reject(reason)
      })
  })
}


Original answer

It doesn't have a specified default; the specification doesn't discuss timeouts at all.

You can implement your own timeout wrapper for promises in general:

// Rough implementation. Untested.
function timeout(ms, promise) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      reject(new Error("timeout"))
    }, ms)
    promise.then(resolve, reject)
  })
}

timeout(1000, fetch('/hello')).then(function(response) {
  // process response
}).catch(function(error) {
  // might be a timeout error
})

As described in https://github.com/github/fetch/issues/175 Comment by https://github.com/mislav

Comments:
2023-01-17 00:47:08
Why is this the accepted answer? The setTimeout here will keep going even if the promise resolves. A better solution would be to do this: github.com/github/fetch/issues/175#issuecomment-216791333
2023-01-17 00:47:08
@radtad I think it's personal preference as to which way you go as the promise cannot be rejected after it has been resolved, so the setTimeout will have no effect here. I personally think it looks a little neater in the solution where the timeout remains
2023-01-17 00:47:08
If the component is destroyed for some reason before the timeout, it can be a problem.
2023-01-17 00:47:08
@radtad mislav defends his approach lower down in that thread: github.com/github/fetch/issues/175#issuecomment-284787564. It doesn't matter that the timeout keeps going, because calling .reject() on a promise that's already been resolved does nothing.
2023-01-17 00:47:09
although the 'fetch' function is rejected by timeout, the background tcp connection is not closed. How can I quit my node process gracefully?
2023-01-17 00:47:09
STOP! This is an incorrect answer! Although, it looks like a good and working solution, but actually the connection will not be closed, which eventually occupies a TCP connection (could be even infinite - depends on the server). Imagine this WRONG solution to be implemented in a system that retries a connection every period of time - This could lead to network interface suffocation (overloading) and make your machine hang eventually! @Endless posted the correct answer here.
2023-01-17 00:47:09
@SlavikMeltser I don't get it. The answer you pointed doesn't break the TCP connection either.
2023-01-17 00:47:09
This will not work if you use await fetch
2023-01-17 00:47:09
@et3rnal You should be able to use const response = await timeout(1000, fetch('/hello'));
2023-01-17 00:47:09
Note that the answer @SlavikMeltser talks about is actually this one with AbortController stackoverflow.com/a/50101022
  fetchTimeout (url,options,timeout=3000) {
    return new Promise( (resolve, reject) => {
      fetch(url, options)
      .then(resolve,reject)
      setTimeout(reject,timeout);
    })
  }
Comments:
2023-01-17 00:47:09
This is pretty much the same as stackoverflow.com/a/46946588/1008999 but you have a default timeout

EDIT: The fetch request will still be running in the background and will most likely log an error in your console.

Indeed the Promise.race approach is better.

See this link for reference Promise.race()

Race means that all Promises will run at the same time, and the race will stop as soon as one of the promises returns a value. Therefore, only one value will be returned. You could also pass a function to call if the fetch times out.

fetchWithTimeout(url, {
  method: 'POST',
  body: formData,
  credentials: 'include',
}, 5000, () => { /* do stuff here */ });

If this piques your interest, a possible implementation would be :

function fetchWithTimeout(url, options, delay, onTimeout) {
  const timer = new Promise((resolve) => {
    setTimeout(resolve, delay, {
      timeout: true,
    });
  });
  return Promise.race([
    fetch(url, options),
    timer
  ]).then(response => {
    if (response.timeout) {
      onTimeout();
    }
    return response;
  });
}

You can create a timeoutPromise wrapper

function timeoutPromise(timeout, err, promise) {
  return new Promise(function(resolve,reject) {
    promise.then(resolve,reject);
    setTimeout(reject.bind(null,err), timeout);
  });
}

You can then wrap any promise

timeoutPromise(100, new Error('Timed Out!'), fetch(...))
  .then(...)
  .catch(...)  

It won't actually cancel an underlying connection but will allow you to timeout a promise.
Reference

Proper error handling tips


Normal practice:

To add timeout support most of the time it is suggested to introduce a Promise utility function like this:

function fetchWithTimeout(resource, { signal, timeout, ...options } = {}) {
  const controller = new AbortController();
  if (signal != null) signal.addEventListener("abort", controller.abort);
  const id = timeout != null ? setTimeout(controller.abort, timeout) : undefined;
  return fetch(resource, {
    ...options,
    signal: controller.signal
  }).finally(() => {
    if (id != null) clearTimeout(id);
  });
}

Calling controller.abort or rejecting the promise inside the setTimeout callback function distorts the stack trace.

This is suboptimal, since one would have to add boilerplate error handlers with log messages in the functions calling the fetch method if post-error log analysis is required.


Good expertise:

To preserve the error along with it's stack trace one can apply the following technique:

function sleep(ms = 0) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(), ms);
  });
}

async function fetchWithTimeout(
  resource,
  { signal, timeout, ...options } = {}
) {
  const controller = new AbortController();
  if (signal != null) signal.addEventListener("abort", controller.abort);
  const request = fetch(
    resource,
    { ...options, signal: controller.signal }
  );
  if (timeout != null) {
    const aborter = sleep(timeout);
    const race = await Promise.race([aborter, request]);
    if (race == null) controller.abort();
  }
  return request;
}

(async () => {
  try {
    await fetchWithTimeout(new URL(window.location.href), { timeout: 5 });
  } catch (error) {
    console.error("Error in test", error);
  }
})();

Using c-promise2 lib the cancellable fetch with timeout might look like this one (Live jsfiddle demo):

import CPromise from "c-promise2"; // npm package

function fetchWithTimeout(url, {timeout, ...fetchOptions}= {}) {
    return new CPromise((resolve, reject, {signal}) => {
        fetch(url, {...fetchOptions, signal}).then(resolve, reject)
    }, timeout)
}
        
const chain = fetchWithTimeout("https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=10s", {timeout: 5000})
    .then(request=> console.log('done'));
    
// chain.cancel(); - to abort the request before the timeout

This code as a npm package cp-fetch

Using AbortController and setTimeout;

const abortController = new AbortController();

let timer: number | null = null;

fetch('/get', {
    signal: abortController.signal, // Content to abortController
})
    .then(res => {
        // response success
        console.log(res);

        if (timer) {
            clearTimeout(timer); // clear timer
        }
    })
    .catch(err => {
        if (err instanceof DOMException && err.name === 'AbortError') {
            // will return a DOMException
            return;
        }

        // other errors
    });

timer = setTimeout(() => {
    abortController.abort();
}, 1000 * 10); // Abort request in 10s.

This is a fragment in @fatcherjs/middleware-aborter.

By using fatcher, it can easy to abort a fetch request.

import { aborter } from '@fatcherjs/middleware-aborter';
import { fatcher, isAbortError } from 'fatcher';

fatcher({
    url: '/bar/foo',
    middlewares: [
        aborter({
            timeout: 10 * 1000, // 10s
            onAbort: () => {
                console.log('Request is Aborted.');
            },
        }),
    ],
})
    .then(res => {
        // Request success in 10s
        console.log(res);
    })
    .catch(err => {
        if (isAbortError(err)) {
            //Run error when request aborted.
            console.error(err);
        }

        // Other errors.
    });