Skip to content

Instantly share code, notes, and snippets.

@john-doherty
Last active December 17, 2021 02:06
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save john-doherty/bcf35d39d8b30d01ae51ccdecf6c94f5 to your computer and use it in GitHub Desktop.
Save john-doherty/bcf35d39d8b30d01ae51ccdecf6c94f5 to your computer and use it in GitHub Desktop.
Adds a timeout to a JavaScript promise, rejects if not resolved within timeout period (uses requestAnimationFrame for better accuracy)
(function () {
'use strict';
/**
* wraps a promise in a timeout, allowing the promise to reject if not resolve with a specific period of time
* @param {integer} ms - milliseconds to wait before rejecting promise if not resolved
* @param {Promise} promise to monitor
* @example
* promiseTimeout(1000, fetch('https://courseof.life/johndoherty.json'))
* .then(function(cvData){
* alert(cvData);
* })
* .catch(function(){
* alert('request either failed or timed-out');
* });
* @returns {Promise} resolves as normal if not timed-out, otherwise rejects
*/
function promiseTimeout(ms, promise) {
return new Promise(function (resolve, reject) {
// create a timeout to reject promise if not resolved
var timer = requestTimeout(function () {
reject(new Error('Promise Timed Out'));
}, ms);
promise.then(function (res) {
clearRequestTimeout(timer);
resolve(res);
})
.catch(function (err) {
clearRequestTimeout(timer);
reject(err);
});
});
}
/* #region helpers */
var requestAnimFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame || function(callback) {
window.setTimeout(callback, 1000 / 60);
};
/**
* Behaves the same as setTimeout except uses requestAnimationFrame() where possible for better performance
* @param {function} fn The callback function
* @param {int} delay The delay in milliseconds
* @returns {object} handle to the timeout object
*/
function requestTimeout(fn, delay) {
if (!window.requestAnimationFrame && !window.webkitRequestAnimationFrame &&
!(window.mozRequestAnimationFrame && window.mozCancelRequestAnimationFrame) && // Firefox 5 ships without cancel support
!window.oRequestAnimationFrame && !window.msRequestAnimationFrame) return window.setTimeout(fn, delay);
var start = new Date().getTime();
var handle = {};
var loop = function() {
var current = new Date().getTime();
var delta = current - start;
if (delta >= delay) {
fn.call();
}
else {
handle.value = requestAnimFrame(loop);
}
};
handle.value = requestAnimFrame(loop);
return handle;
}
/**
* Behaves the same as clearTimeout except uses cancelRequestAnimationFrame() where possible for better performance
* @param {object} handle The callback function
* @returns {void}
*/
function clearRequestTimeout(handle) {
if (handle) {
window.cancelAnimationFrame ? window.cancelAnimationFrame(handle.value) :
window.webkitCancelAnimationFrame ? window.webkitCancelAnimationFrame(handle.value) :
window.webkitCancelRequestAnimationFrame ? window.webkitCancelRequestAnimationFrame(handle.value) : /* Support for legacy API */
window.mozCancelRequestAnimationFrame ? window.mozCancelRequestAnimationFrame(handle.value) :
window.oCancelRequestAnimationFrame ? window.oCancelRequestAnimationFrame(handle.value) :
window.msCancelRequestAnimationFrame ? window.msCancelRequestAnimationFrame(handle.value) :
clearTimeout(handle);
}
}
/* #endregion */
if (typeof window === 'undefined') {
module.exports = promiseTimeout;
}
else {
window.promiseTimeout = promiseTimeout;
}
})();
@0xFFFC
Copy link

0xFFFC commented Mar 25, 2019

When the timeout is reached, I think that we should not longer trigger the resolve or the reject function.

`let promiseTimeout = (ms, promise, step) => {
return new Promise(function(resolve, reject) {
// create a timeout to reject promise if not resolved

    let cleared = false;
    let timer = setTimeout(function() {cleared = true; reject(step + " time out");}, ms);

    promise
    .then(function(res){
        // If already cleared, the response is too late, we must not do anything
        if (! cleared) {
            clearTimeout(timer);
            resolve(res);
        }    
    })
    .catch(function(err){
        if (! cleared) {
            clearTimeout(timer);
            reject(err);
        }    
    });
});

};`

@jack-sf
Copy link

jack-sf commented Jun 13, 2019

Some version with tweak to change the produced error and a onTimeout callback if you want to do something once timeout happens, also typescript added:

/**
 * Wraps a promise in a timeout, allowing the promise to reject if not resolved with a specific period of time.
 *
 * @example
 * wrapPromiseWithTimeout(1000, fetch('https://courseof.life/johndoherty.json'))
 *   .then(function(cvData){
 *     alert(cvData);
 *   })
 *   .catch(function(){
 *     alert('request either failed or timedout');
 *   });
 */
function wrapPromiseWithTimeout<T>(
  promise: Promise<T>,
  time: number,
  {
    error = () => new Error('promise timeout'),
    onTimeout,
  }: {
    error?(): Error;
    onTimeout?(): void;
  } = {},
) {
  return new Promise<T>((resolve, reject) => {
    const timer = setTimeout(() => {
      if (onTimeout) {
        onTimeout();
      }

      reject(error());
    }, time);

    promise
      .then(res => {
        clearTimeout(timer);
        resolve(res);
      })
      .catch(err => {
        clearTimeout(timer);
        reject(err);
      });
  });
}

@im-danwu
Copy link

Promises can only be settled once.

const bar = await new Promise(resolve => { resolve(1); resolve(2); }); here bar is 1

So you don't need all the clearTimeout or worry about race conditions 👍

function promiseTimeout(ms, promise) {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error("promise timeout")), ms);
    promise.then(resolve).catch(reject);
  });
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment