NodeJS Timeout a Promise if failed to complete in time
Native JavaScript promises don't have any timeout mechanism.
The question about your implementation would probably be a better fit for http://codereview.stackexchange.com, but a couple of notes:
-
You don't provide a means of actually doing anything in the promise, and
-
There's no need for
clearTimeout
within yoursetTimeout
callback, sincesetTimeout
schedules a one-off timer. -
Since a promise can't be resolved/rejected once it's been resolved/rejected, you don't need that check.
So continuing your myPromise
function approach, perhaps something like this:
function myPromise(timeout, callback) {
return new Promise((resolve, reject) => {
// Set up the timeout
const timer = setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout} ms`));
}, timeout);
// Set up the real work
callback(
(value) => {
clearTimeout(timer);
resolve(value);
},
(error) => {
clearTimeout(timer);
reject(error);
}
);
});
}
Used like this:
myPromise(2000, (resolve, reject) => {
// Real work is here
});
(Or possibly slightly less complicated, see under the divider below.)
I'd be slightly concerned about the fact that the semantics are different (no new
, whereas you do use new
with the Promise
constructor). But the bigger issue with it is that it assumes you're always creating a promise from scratch, but you usually want to be able to use a promise you already have.
You can deal with both issues by subclassing Promise
:
class MyPromise extends Promise {
constructor(timeout, callback) {
// We need to support being called with no milliseconds
// value, because the various Promise methods (`then` and
// such) correctly call the subclass constructor when
// building the new promises they return.
const haveTimeout = typeof timeout === "number";
const init = haveTimeout ? callback : timeout;
super((resolve, reject) => {
if (haveTimeout) {
const timer = setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout}ms`));
}, timeout);
init(
(value) => {
clearTimeout(timer);
resolve(value);
},
(error) => {
clearTimeout(timer);
reject(error);
}
);
} else {
init(resolve, reject);
}
});
}
// Pick your own name of course. (You could even override `resolve` itself
// if you liked; just be sure to do the same arguments detection we do
// above in the constructor, since you need to support the standard use of
// `resolve`.)
static resolveWithTimeout(timeout, x) {
if (!x || typeof x.then !== "function") {
// `x` isn't a thenable, no need for the timeout,
// fulfill immediately
return this.resolve(x);
}
return new this(timeout, x.then.bind(x));
}
}
Usage (if constructing a new promise):
let p = new MyPromise(300, (resolve, reject) => {
// ...
});
p.then((value) => {
// ...
})
.catch((error) => {
// ...
});
Usage (if using a promise you already have):
MyPromise.resolveWithTimeout(100, somePromiseYouAlreadyHave)
.then((value) => {
// ...
})
.catch((error) => {
// ...
});
Live Example:
"use strict";
class MyPromise extends Promise {
constructor(timeout, callback) {
// We need to support being called with no milliseconds
// value, because the various Promise methods (`then` and
// such) correctly call the subclass constructor when
// building the new promises they return.
const haveTimeout = typeof timeout === "number";
const init = haveTimeout ? callback : timeout;
super((resolve, reject) => {
if (haveTimeout) {
const timer = setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout}ms`));
}, timeout);
init(
(value) => {
clearTimeout(timer);
resolve(value);
},
(error) => {
clearTimeout(timer);
reject(error);
}
);
} else {
init(resolve, reject);
}
});
}
// Pick your own name of course. (You could even override `resolve` itself
// if you liked; just be sure to do the same arguments detection we do
// above in the constructor, since you need to support the standard use of
// `resolve`.)
static resolveWithTimeout(timeout, x) {
if (!x || typeof x.then !== "function") {
// `x` isn't a thenable, no need for the timeout,
// fulfill immediately
return this.resolve(x);
}
return new this(timeout, x.then.bind(x));
}
}
// Some functions for the demonstration
const neverSettle = () => new Promise(() => {});
const fulfillAfterDelay = (delay, value) => new Promise((resolve) => setTimeout(resolve, delay, value));
const rejectAfterDelay = (delay, error) => new Promise((resolve, reject) => setTimeout(reject, delay, error));
const examples = [
function usageWhenCreatingNewPromise1() {
console.log("Showing timeout when creating new promise");
const p = new MyPromise(100, (resolve, reject) => {
// We never resolve/reject, so we test the timeout
});
return p.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
function usageWhenCreatingNewPromise2() {
console.log("Showing when the promise is fulfilled before the timeout");
const p = new MyPromise(100, (resolve, reject) => {
setTimeout(resolve, 50, "worked");
});
return p.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
function usageWhenCreatingNewPromise3() {
console.log("Showing when the promise is rejected before the timeout");
const p = new MyPromise(100, (resolve, reject) => {
setTimeout(reject, 50, new Error("failed"));
});
return p.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
function usageWhenYouAlreadyHaveAPromise1() {
console.log("Showing timeout when using a promise we already have");
return MyPromise.resolveWithTimeout(100, neverSettle())
.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
function usageWhenYouAlreadyHaveAPromise2() {
console.log("Showing fulfillment when using a promise we already have");
return MyPromise.resolveWithTimeout(100, fulfillAfterDelay(50, "worked"))
.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
function usageWhenYouAlreadyHaveAPromise3() {
console.log("Showing rejection when using a promise we already have");
return MyPromise.resolveWithTimeout(100, rejectAfterDelay(50, new Error("failed")))
.then((value) => {
console.log(`Fulfilled: ${value}`);
})
.catch((error) => {
console.log(`Rejected: ${error}`);
});
},
async function usageInAnAsyncFunction1() {
console.log("Showing timeout in async function");
try {
const value = await MyPromise.resolveWithTimeout(100, neverSettle());
console.log(`Fulfilled: ${value}`);
} catch (error) {
console.log(`Rejected: ${error}`);
}
},
async function usageInAnAsyncFunction2() {
console.log("Showing fulfillment in async function");
try {
const value = await MyPromise.resolveWithTimeout(100, fulfillAfterDelay(50, "worked"));
console.log(`Fulfilled: ${value}`);
} catch (error) {
console.log(`Rejected: ${error}`);
}
},
async function usageInAnAsyncFunction3() {
console.log("Showing rejection in async function");
try {
const value = await MyPromise.resolveWithTimeout(100, rejectAfterDelay(50, new Error("failed")));
console.log(`Fulfilled: ${value}`);
} catch (error) {
console.log(`Rejected: ${error}`);
}
},
];
(async () => {
for (const example of examples) {
try {
await example();
} catch (e) {
}
}
})();
/* Shows the cosole full height in the snippet */
.as-console-wrapper {
max-height: 100% !important;
}
The code above actively cancels the timer when either resolving or rejecting the promise. That may not be necessary depending on your use case, and complicates the code a bit. It's not necessary for the promise part of things; once a promise is resolved or rejected, that cannot be changed, calling the resolve
or reject
function again has no effect on the promise (the spec is clear about that). But if you don't cancel the timer, the timer is still pending until it fires. A pending promise will keep Node.js from exiting, for instance, so if you did this with a long timeout on the last thing you were doing, it could pointlessly delay exiting the process. Browsers don't delay leaving the page with pending timers, so that doesn't apply to browsers. So again, your mileage may vary and you may be able to simplify a bit by not cancelling the timer.
If you didn't care about the pending timer, MyPromise
would be a simpler:
class MyPromise extends Promise {
constructor(timeout, callback) {
// We need to support being called with no milliseconds
// value, because the various Promise methods (`then` and
// such) correctly call the subclass constructor when
// building the new promises they return.
const haveTimeout = typeof timeout === "number";
const init = haveTimeout ? callback : timeout;
super((resolve, reject) => {
init(resolve, reject);
if (haveTimeout) {
setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout}ms`));
}, timeout);
}
});
}
// Pick your own name of course. (You could even override `resolve` itself
// if you liked; just be sure to do the same arguments detection we do
// above in the constructor, since you need to support the standard use of
// `resolve`.)
static resolveWithTimeout(timeout, x) {
if (!x || typeof x.then !== "function") {
// `x` isn't a thenable, no need for the timeout,
// fulfill immediately
return this.resolve(x);
}
return new this(timeout, x.then.bind(x));
}
}
While maybe there's no support for a promise timeout, you could race promises:
var race = Promise.race([
new Promise(function(resolve){
setTimeout(function() { resolve('I did it'); }, 1000);
}),
new Promise(function(resolve, reject){
setTimeout(function() { reject('Timed out'); }, 800);
})
]);
race.then(function(data){
console.log(data);
}).catch(function(e){
console.log(e);
});
A generic Promise.timeout
:
Promise.timeout = function(timeout, cb){
return Promise.race([
new Promise(cb),
new Promise(function(resolve, reject){
setTimeout(function() { reject('Timed out'); }, timeout);
})
]);
}
Example:
Promise.timeout = function(timeout, cb) {
return Promise.race([
new Promise(cb),
new Promise(function(resolve, reject) {
setTimeout(function() {
reject('Timed out');
}, timeout);
})
]);
}
function delayedHello(cb){
setTimeout(function(){
cb('Hello');
}, 1000);
}
Promise.timeout(800, delayedHello).then(function(data){
console.log(data);
}).catch(function(e){
console.log(e);
}); //delayedHello doesn't make it.
Promise.timeout(1200, delayedHello).then(function(data){
console.log(data);
}).catch(function(e){
console.log(e);
}); //delayedHello makes it.
Might be a little bit costly, because you are actually creating 3 promises instead of 2. I think it's clearer this way though.
You might want to setup a promise instead of having the function construct it for you. This way you separate concerns and you are ultimately focused on racing your promise against a newly built promise that will reject at x
miliseconds.
Promise.timeout = function(timeout, promise){
return Promise.race([
promise,
new Promise(function(resolve, reject){
setTimeout(function() { reject('Timed out'); }, timeout);
})
]);
}
How to use:
var p = new Promise(function(resolve, reject){
setTimeout(function() { resolve('Hello'); }, 1000);
});
Promise.timeout(800, p); //will be rejected, as the promise takes at least 1 sec.