timeoutFetch(url, {
method: 'post',
credentials: 'same-origin',
timeout: 8000 // optional
}).then(checkResponseStatus)
.catch(, error => {
// could be fetch() error or timeout error
})
timeoutFetch(url, {
method: 'post',
credentials: 'same-origin',
timeout: 8000 // optional
}).then(checkResponseStatus)
.catch(error => {
// could be fetch() error or timeout error
})
timeoutFetch(url, {
method: 'post',
credentials: 'same-origin',
timeout: 8000 // optional
}).then(checkResponseStatus, error => {
// could be fetch() error or timeout error
})
Suggested ES6 rewrite
function timeoutFetch (input, init = {}) {
const timeout = init && Number(init.timeout) || 8000
return new Promise((resolve, reject) => {
fetch(input, init).then(resolve, reject)
setTimeout(() => reject(new TypeError('Client timed out')), timeout)
})
}
Invocation
timeoutFetch(url, {
method: 'post',
credentials: 'same-origin',
timeout: 8000 // optional
})
.then(checkResponseStatus)
.catch(error => {
// could be fetch() error or timeout error
})
Explanation
function timeoutFetch (input, init = {})
Here, we are mimicking the signature of the native fetch
API, which has one required argument and one optional argument. We simply expect a timeout
property to optionally exist on init
within this wrapper function.
const timeout = init && Number(init.timeout) || 8000
This first checks that init
is "truthy", and then checks that init.timeout
is a valid, non-zero number. If these are satisfied, then timeout
is assigned the value of the supplied property, otherwise it defaults to 8000
milliseconds, like in your example.
return new Promise((resolve, reject) => { ... })
If you are at all familiar with using promises, then you'll recognize this pattern. While it is typically considered an anti-pattern, this particular implementation is written properly, and is also necessary in this case to take advantage of a convenient implicit race-condition behavior of promises that I'll explain in a moment.
fetch(input, init).then(resolve, reject)
This line invokes the native fetch()
method with the wrapper's arguments and resolves the explicitly constructed promise with the fulfilled Response
object, if, and only if, it is successful and it completes before the setTimeout()
callback is invoked.
The reason this occurs is because of the specification: a promise can only be fulfilled, rejected, or remain pending forever, and if it is fulfilled or rejected, it is "settled" and cannot be fulfilled or rejected again.
If it is unsuccessful, and it fails before the timeout occurs, then it will invoke reject()
with an error.
setTimeout(() => ...), timeout)
This part is pretty straightforward; we're creating a timeout given the amount of milliseconds supplied from init.timeout
or the default 8000
, and invoking the callback function in that amount of time.
reject(new TypeError('Client timed out'))
Inside the callback, we're rejecting the constructed promise with a TypeError
, but keep in mind, if the fetch()
function has already invoked resolve()
or reject()
in its then()
, this call is essentially a noop because the constructed promise has already locked into its state and cannot be "settled" again. Because of this, it is unnecessary to assign a reference to the timeout and call clearTimeout()
if those occur first.
Conclusion
If you've read this far, you've probably realized by now that the suggested code is compact because it takes advantage of a thorough understanding of the promise specification, and is able to safely make assumptions that keeps the code DRY.