Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
82 views
in Technique[技术] by (71.8m points)

javascript - How to write an asynchronous (promise-based) function that times out?

I want to have function which will do some work, if then is some condition true, then resolve promise. If not, wait little bit (lets say one second) and then try it again. Beside that if time limit expired, promise will reject. How can do I that? Look at my code example...If I wrap entire function in return new Promise(), I cant use await inside of it (which I need...I need to sleep some time if condition fail, and also I need wait to my await at end of my function).

Here is my example of code:

async funcWithTimeLimit(numberToDecrement, timeLimit){
    let sleep = undefined;
    let timeLimitTimeout = setTimeout(() => { 
        if (sleep)
            clearTimeout(sleep); 
        return Promise.reject("Time limit of " + (timeLimit/1000) +" secs expired"); //reject
    }, timeLimit);

    while(numberToDecrement > 0){
        for(let i = 10;(i > 0 && numberToDecrement > 0); i--){ //Do some work
            numberToDecrement--;
        }

        if(numberToDecrement > 0){
            await new Promise(resolve => sleep = setTimeout(resolve, 1000));
        }
    }

    clearTimeout(timeLimitTimeout);
    await new Promise((resolve, reject) => sleep = setTimeout(resolve, 500)); // Do something
    return ""; //resolve
}

NOTE: My biggest problem is - how can I write this function to be able to catch rejection (at place where I call funcWithTimeLimit()) in case of time limit expiration?

question from:https://stackoverflow.com/questions/65871964/how-to-write-an-asynchronous-promise-based-function-that-times-out

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

There are two basic approaches. One is to repeatedly check the time and throw an exception once you hit the limit, the other is to race each awaited promise against a timeout. A third would require cooperation by the asynchronous tasks that you start, if they give you a way to cancel them in their interface, to simply offload the task of checking for timeout to them.

In neither of them you can reject the async function by throwing an exception from a setTimeout.

  1. Rather simple:

    function doSomething() {
        return new Promise((resolve, reject) => {
            setTimeout(resolve, 500)); // Do something
        }
    }
    async funcWithTimeLimit(numberToDecrement, timeLimit) {
        while (numberToDecrement > 0) {
            for (let i = 10; i > 0 && numberToDecrement > 0; i--) {
                if (Date.now() > timelimit) throw new TimeoutError("Exceeded limit");
    //          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                numberToDecrement--; // Do some work
            }
    
            if (numberToDecrement > 0) {
                if (Date.now() + 1000 > timelimit) throw new TimeoutError("Exceeded limit");
    //          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                await new Promise(resolve => setTimeout(resolve, 1000));
            }
        }
        // but ineffective here:
        await doSomething();
        return "";
    }
    
  2. Racing:

    async funcWithTimeLimit(numberToDecrement, timeLimit) {
        const timeout = new Promise((resolve, reject) => {
            setTimeout(() => {
                reject(new TimeoutError("Exceeded limit"));
            }, timeLimit - Date.now());
        });
        timeout.catch(e => void e); // avoid unhandled promise rejection if not needed
        while (numberToDecrement > 0) {
            for (let i = 10; i > 0 && numberToDecrement > 0; i--) {
                // no guarding of synchronous loops
                numberToDecrement--; // Do some work
            }
    
            if (numberToDecrement > 0) {
                await Promise.race([ timeout,
    //          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                    new Promise(resolve => setTimeout(resolve, 1000)),
                ]);
            }
        }
        await Promise.race([ timeout,
    //  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            doSomething(),
        ]);
        return "";
    }
    

    Notice that the setTimeout keeps the event loop open if when the function already completed, we just ignore the rejected promise. Better, but more laborious, is to cancel the timeout when we don't need it any longer:

    async funcWithTimeLimit(numberToDecrement, timeLimit) {
        let timer;
        const timeout = new Promise((resolve, reject) => {
            timer = setTimeout(() => {
                reject(new TimeoutError("Exceeded limit"));
            }, timeLimit - Date.now());
        });
        try {
            // as before:
            while (numberToDecrement > 0) {
                for (let i = 10; i > 0 && numberToDecrement > 0; i--) {
                    numberToDecrement--; // Do some work
                }
    
                if (numberToDecrement > 0) {
                    await Promise.race([ timeout, new Promise(resolve => setTimeout(resolve, 1000)) ]);
                }
            }
            await Promise.race([ timeout, doSomething() ]);
            return "";
        } finally {
            clearTimeout(timer);
    //      ^^^^^^^^^^^^^^^^^^^^
        }
    }
    
  3. With cooperation from the called function it's better of course:

    function delay(t, limit) {
        if (Date.now() + t > timelimit) throw new TimeoutError("Exceeded limit");
        return new Promise(resolve => setTimeout(resolve, t));
    }
    function doSomething(limit) {
        return new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                xhr.cancel(); // custom API-dependent cancellation
                reject(new TimeoutError("Exceeded limit"));
            }, limit - Date.now());
            const xhr = someApiCall(); // Do something
            xhr.onerror = err => { clearTimeout(timeout); reject(err); };
            xhr.onload = res => { clearTimeout(timeout); resolve(res); };
        }
    }
    async funcWithTimeLimit(numberToDecrement, timeLimit) {
        while (numberToDecrement > 0) {
            for (let i = 10; i > 0 && numberToDecrement > 0; i--) {
                numberToDecrement--; // Do some work
            }
    
            if (numberToDecrement > 0) {
                await delay(1000, timeLimit);
    //                            ^^^^^^^^^
            }
        }
        await doSomething(timeLimit);
    //                    ^^^^^^^^^
        return "";
    }
    

You can (where applicable) and should (where sensible) combine these approaches of course.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...