241

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?

Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
Akshay Lokur
  • 6,680
  • 13
  • 43
  • 62

14 Answers14

288

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.abort(), 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

killjoy
  • 3,665
  • 1
  • 19
  • 16
Endless
  • 34,080
  • 13
  • 108
  • 131
  • 30
    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. – Karl Adler Nov 12 '18 at 15:16
  • 7
    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. – Estus Flask Nov 17 '18 at 11:05
  • 4
    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. – Aurelio Dec 05 '18 at 09:44
  • 3
    "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. – Jay Wick Feb 17 '19 at 20:38
  • 19
    Better to have this answer than no answer because people are put-off by nitpickery, tbh – Michael Terry Feb 18 '19 at 22:17
  • 1
    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? – Trevor Sep 20 '20 at 01:00
  • 4
    @EstusFlask currently it is [wide supported](https://caniuse.com/?search=AbortController) – Kamil Kiełczewski Feb 23 '21 at 05:54
  • 2
    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: https://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); } }); – Michael Schmitz Feb 01 '22 at 10:02
  • 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()`). – Константин Ван Jun 06 '22 at 08:57
  • 1
    @КонстантинВан you can't clear the timeout for `AbortSignal.timeout` you have to use AbortController for manually canceling – Endless Jun 06 '22 at 11:02
  • @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. – Bluefire Jan 16 '23 at 19:20
  • @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 – Endless Jan 16 '23 at 22:40
182

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
})
Endless
  • 34,080
  • 13
  • 108
  • 131
Karl Adler
  • 15,780
  • 10
  • 70
  • 88
  • 2
    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. – lionello Jun 22 '19 at 00:36
  • 13
    IMHO this could be improved futher with AbortController when rejecting, see https://stackoverflow.com/a/47250621. – RiZKiT Oct 14 '19 at 11:24
  • 2
    It would be better to clear the timeout if fetch is successful as well. – Bob9630 Apr 23 '20 at 00:20
  • 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 – ofarukcaki Jul 23 '21 at 13:25
  • 1
    For reference, AbortController is not available server side – jfunk May 15 '22 at 21:37
  • 2
    @jfunk that's not true. AbortController is available in Nodejs v15 and above. – Abhinav Saini Jan 05 '23 at 18:32
96

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

zhirzh
  • 3,273
  • 3
  • 25
  • 30
shakeel
  • 1,609
  • 1
  • 14
  • 14
  • 34
    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: https://github.com/github/fetch/issues/175#issuecomment-216791333 – radtad Mar 22 '19 at 20:28
  • @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 – LarryFisherman Apr 01 '19 at 08:30
  • If the component is destroyed for some reason before the timeout, it can be a problem. – Aderbal Nunes Jun 10 '19 at 13:59
  • 3
    @radtad mislav defends his approach lower down in that thread: https://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. – Mark Amery Sep 11 '19 at 10:25
  • 1
    although the 'fetch' function is rejected by timeout, the background tcp connection is not closed. How can I quit my node process gracefully? – progquester Dec 12 '19 at 11:57
  • 41
    **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](https://stackoverflow.com/a/49857905/1291121). – Slavik Meltser Dec 22 '19 at 14:09
  • 3
    @SlavikMeltser I don't get it. The answer you pointed doesn't break the TCP connection either. – Mateus Pires Oct 23 '20 at 13:35
  • This will not work if you use `await fetch` – et3rnal Dec 14 '20 at 00:42
  • @et3rnal You should be able to use `const response = await timeout(1000, fetch('/hello'));` – Wayne Ellery Dec 15 '20 at 00:31
  • Note that the answer @SlavikMeltser talks about is actually this one with `AbortController` https://stackoverflow.com/a/50101022 – Maciej Krawczyk Aug 10 '21 at 18:11
77

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
        }
    });
starball
  • 20,030
  • 7
  • 43
  • 238
Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • 4
    This is fantastic! It covers all the nasty edge cases that were problematic in other answers, _and_ you provide a clear usage example. – Atte Juvonen Dec 01 '20 at 18:38
  • 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). – vdavid Feb 23 '22 at 10:18
  • @vdavid I'm not sure I follow what you mean. Can you give me a code example with comments describing your problem? – Aadit M Shah Aug 02 '22 at 01:29
  • @AaditMShah This was a rather dumb question from someone who had spent too much time with C/C++ prior to asking. It's pretty clear that the lifespan of `controller` does not end at the end of the function which created it. – vdavid Mar 20 '23 at 16:39
17

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');
    }
}

aGuegu
  • 1,813
  • 1
  • 21
  • 22
13

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);
      }
    });
  }
Endless
  • 34,080
  • 13
  • 108
  • 131
code-jaff
  • 9,230
  • 4
  • 35
  • 56
  • i like this one better, less repetitive to use more than once. – dandavis Oct 26 '17 at 05:48
  • 4
    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. – trysis Feb 01 '18 at 20:39
  • 2
    @trysis well, yes. Recently implemented a solution for abort fetch with [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController), but still experimental with limited browser support. [Discussion](https://github.com/whatwg/fetch/issues/447) – code-jaff Feb 03 '18 at 05:42
  • That's funny, IE & Edge are the only ones that support it! Unless the mobile Mozilla site is acting up again... – trysis Feb 03 '18 at 06:32
  • Firefox has been supporting it since 57. ::watching at Chrome:: – Franklin Yu Feb 25 '18 at 18:50
  • @FranklinYu Chrome supported it a few months later than when you added this comment :) Currently AbortController is widely supported (by ~all browsers). https://developer.mozilla.org/en-US/docs/Web/API/AbortController – aderchox Mar 13 '22 at 08:52
13

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

Harikrishnan
  • 9,688
  • 11
  • 84
  • 127
9

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;
  });
}
Clyde
  • 7,389
  • 5
  • 31
  • 57
Arro
  • 158
  • 1
  • 10
5

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);
});
Endless
  • 34,080
  • 13
  • 108
  • 131
8bitjunkie
  • 12,793
  • 9
  • 57
  • 70
3

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.
    });
Fansy
  • 79
  • 3
1

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

Tsvetan Ganev
  • 8,246
  • 4
  • 26
  • 43
Pulkit Aggarwal
  • 2,554
  • 4
  • 23
  • 33
1

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, signal) {
  return new Promise((resolve, reject) => {
    const id = setTimeout(() => resolve(), ms);
    signal?.addEventListener("abort", () => {
      clearTimeout(id);
      reject();
    });
  });
}

async function fetch(
  resource,
  options
) {
  const { timeout, signal, ...ropts } = options ?? {};

  const controller = new AbortController();
  let sleepController;
  try {
    signal?.addEventListener("abort", () => controller.abort());

    const request = nodeFetch(resource, {
      ...ropts,
      signal: controller.signal,
    });

    if (timeout != null) {
      sleepController = new AbortController();
      const aborter = sleep(timeout, sleepController.signal);
      const race = await Promise.race([aborter, request]);
      if (race == null) controller.abort();
    }

    return request;
  } finally {
    sleepController?.abort();
  }
}

(async () => {
  try {
    await fetchWithTimeout(new URL(window.location.href), { timeout: 5 });
  } catch (error) {
    console.error("Error in test", error);
  }
})();
Sim Dim
  • 552
  • 3
  • 9
0
  fetchTimeout (url,options,timeout=3000) {
    return new Promise( (resolve, reject) => {
      fetch(url, options)
      .then(resolve,reject)
      setTimeout(reject,timeout);
    })
  }
Mojimi
  • 2,561
  • 9
  • 52
  • 116
  • This is pretty much the same as https://stackoverflow.com/a/46946588/1008999 but you have a default timeout – Endless May 13 '19 at 14:07
-1

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

Dmitriy Mozgovoy
  • 1,419
  • 2
  • 8
  • 7