Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NodeJS Timeout a Promise if failed to complete in time

How can I timeout a promise after certain amount of time? I know Q has a promise timeout, but I'm using native NodeJS promises and they don't have .timeout function.

Am I missing one or its wrapped differently?

Alternatively, Is the below implementation good in means of not sucking up memory, actually working as expected?

Also can I make it somehow wrapped globally so I can use it for every promise I create, without having to repeat the setTimeout and clearTimeout code?

function run() {     logger.info('DoNothingController working on process id {0}...'.format(process.pid));      myPromise(4000)         .then(function() {             logger.info('Successful!');         })         .catch(function(error) {             logger.error('Failed! ' + error);         }); }  function myPromise(ms) {     return new Promise(function(resolve, reject) {         var hasValueReturned;         var promiseTimeout = setTimeout(function() {             if (!hasValueReturned) {                 reject('Promise timed out after ' + ms + ' ms');             }         }, ms);          // Do something, for example for testing purposes         setTimeout(function() {             resolve();             clearTimeout(promiseTimeout);         }, ms - 2000);     }); } 

Thanks!

like image 699
AlexD Avatar asked Sep 08 '15 15:09

AlexD


People also ask

How do you timeout a promise?

Promise timeoutsWhen you use await or attach a function with . then() , it will wait until the Promise is either resolved or rejected. This is usually not a problem as most async tasks finish within a reasonable time and their result is needed.

What happens if promise is not resolved?

A promise is just an object with properties in Javascript. There's no magic to it. So failing to resolve or reject a promise just fails to ever change the state from "pending" to anything else. This doesn't cause any fundamental problem in Javascript because a promise is just a regular Javascript object.

What happens when promise all fails?

all fail-fast behavior. Promise. all is rejected if any of the elements are rejected. For example, if you pass in four promises that resolve after a timeout and one promise that rejects immediately, then Promise.

Does Nodejs have setTimeout?

The setTimeout function is used to call a function after the specified number of milliseconds. The delay of the called function begins after the remaining statements in the script have finished executing. The setTimeout function is found in the Timers module of Node. js.


1 Answers

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:

  1. You don't provide a means of actually doing anything in the promise, and

  2. There's no need for clearTimeout within your setTimeout callback, since setTimeout schedules a one-off timer.

  3. 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));     } } 
like image 175
T.J. Crowder Avatar answered Oct 01 '22 14:10

T.J. Crowder