JavaScript fetch with Timeout

By  on  

The fetch API started out as a target for criticism because of lack of timeout and request cancelation.  While those criticisms could be argued as fair or not, you can't deny that the fetch API has been pretty awesome.  As we've always done, if a feature is missing, we can always shim it in.

I've recently been thinking about shimming in a fetch timeout and found a good fetch / timeout script here.  I've slightly modified it to prevent the fetch call's then and catch callbacks from carrying out their tasks because I believe the timeout should be handled by the shim's Promise:

const FETCH_TIMEOUT = 5000;
let didTimeOut = false;

new Promise(function(resolve, reject) {
    const timeout = setTimeout(function() {
        didTimeOut = true;
        reject(new Error('Request timed out'));
    }, FETCH_TIMEOUT);
    
    fetch('https://davidwalsh.name/?xx1')
    .then(function(response) {
        // Clear the timeout as cleanup
        clearTimeout(timeout);
        if(!didTimeOut) {
            console.log('fetch good! ', response);
            resolve(response);
        }
    })
    .catch(function(err) {
        console.log('fetch failed! ', err);
        
        // Rejection already happened with setTimeout
        if(didTimeOut) return;
        // Reject with error
        reject(err);
    });
})
.then(function() {
    // Request success and no timeout
    console.log('good promise, no timeout! ');
})
.catch(function(err) {
    // Error: response error, request timeout or runtime error
    console.log('promise error! ', err);
});

Wrapping this code in a function called fetchWithTimeout, whereby you pass in a timeout and fetch URL/settings would work well; since people like to use fetch in a variety of ways, I've chosen not to create a generalized function and instead am just providing the basic logic.

Many would argue that the timeout should come from the server but we all know us front-end devs don't always have control over both sides of a request.  If you're looking for a fetch request timeout snippet, here you go!

Recent Features

  • By
    Regular Expressions for the Rest of Us

    Sooner or later you'll run across a regular expression. With their cryptic syntax, confusing documentation and massive learning curve, most developers settle for copying and pasting them from StackOverflow and hoping they work. But what if you could decode regular expressions and harness their power? In...

  • By
    CSS Gradients

    With CSS border-radius, I showed you how CSS can bridge the gap between design and development by adding rounded corners to elements.  CSS gradients are another step in that direction.  Now that CSS gradients are supported in Internet Explorer 8+, Firefox, Safari, and Chrome...

Incredible Demos

  • By
    Adding Events to Adding Events in MooTools

    Note: This post has been updated. One of my huge web peeves is when an element has click events attached to it but the element doesn't sport the "pointer" cursor. I mean how the hell is the user supposed to know they can/should click on...

  • By
    Unicode CSS Classes

    CSS class name structure and consistency is really important; some developers camelcase classnames, others use dashes, and others use underscores.  One thing I've learned when toying around by HTML and CSS class names is that you can actually use unicode symbols and icons as classnames.

Discussion

  1. Drew

    The core problem with fetch when it comes to cancelation or timeout is baked into the underlying interface: Promises. Stateful, eager Promises just don’t model cancelation very well because the concept of a chain-able future value and potential side-effects of calling for and having that future value resolve/error are too tightly coupled.

    Newer apis seem to be adopting Promises as their model for async requests a bit too glibly, I think, not really understanding this structural problem. Modeling timeouts as errors might make sense in a lot of cases (though not all), but modeling cancelations as such is really problematic.

  2. Ransom

    After the first time resolve() or reject() is called, subsequent calls to resolve() or reject() do absolutely nothing. Therefore, you don’t need to check whether the timeout has completed in order to keep from calling reject() or resolve().

    Also, cancelling the timeout can be done more cleanly, using Promise.prototype.finally.

    With those two items in mind, here’s what the code would look like, wrapped in a function.

    function fetchWithTimeout( url, timeout ) {
        return new Promise( (resolve, reject) => {
            // Set timeout timer
            let timer = setTimeout(
                () => reject( new Error('Request timed out') ),
                timeout
            );
    
            fetch( url ).then(
                response => resolve( response ),
                err => reject( err )
            ).finally( () => clearTimeout(timer) );
        })
    }
    

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!