Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

in JavaScript, how to wrap a promise in timeout?

Tags:

It's a common pattern to implement timeout of some asynchronous function, using deffered/promise:

// Create a Deferred and return its Promise
function timeout(funct, args, time) {
    var dfd = new jQuery.Deferred();

    // execute asynchronous code
    funct.apply(null, args);

    // When the asynchronous code is completed, resolve the Deferred:
    dfd.resolve('success');

    setTimeout(function() {
        dfd.reject('sorry');
    }, time);
    return dfd.promise();
}

Now we can execute some asynchronous function called myFunc and handle timeout:

// Attach a done and fail handler for the asyncEvent
$.when( timeout(myFunc, [some_args], 1000) ).then(
    function(status) {
        alert( status + ', things are going well' );
    },
    function(status) {
        alert( status + ', you fail this time' );
    }
);

OK, let's make a twist in this story! Imagine that the myFunc itself returns a promise (NOTE: promise NOT deferred and I can't change it):

function myFunc(){
    var dfd = new jQuery.Deffered();
    superImportantLibrary.doSomething(function(data)){
       if(data.length < 5){
            dfd.reject('too few data');
       }
       else{
           dfd.resolve('success!');
       }
    }, {'error_callback': function(){
        dfd.reject("there was something wrong but it wasn't timeout");}
    }});
    return dfd.promise();
}

Now if I wrap myFunc in timeout, I will loose the ability to handle errors different then timeout. If myFunc emit progress events, I will loose this as well.

So the question is: how to modify timeout function so it can accept functions returning promises without loosing their errors/progress information?

like image 696
mnowotka Avatar asked Jun 12 '14 11:06

mnowotka


People also ask

How do you wrap a timeout in a promise?

setTimeout() is not exactly a perfect tool for the job, but it's easy enough to wrap it into a promise: const awaitTimeout = delay => new Promise(resolve => setTimeout(resolve, delay)); awaitTimeout(300). then(() => console.

How do you wait for a promise in JavaScript?

The keyword await is used to wait for a Promise. It can only be used inside an async function. This keyword makes JavaScript wait until that promise settles and returns its result.

How do I create a timeout in JavaScript?

The setTimeout() method executes a block of code after the specified time. The method executes the code only once. The commonly used syntax of JavaScript setTimeout is: setTimeout(function, milliseconds);

Why promise is executed before setTimeout?

The reason the promise is executing before your timeout is that the promise isn't actually waiting for anything so it resolved right away. @frankies That has more to do with the way Promises are queued and resolved. The focus of my answer is the difference between setTimeout and Promise .


2 Answers

function timeout(funct, args, time) {
    var deferred = new jQuery.Deferred(),
        promise = funct.apply(null, args);

    if (promise) {
        $.when(promise)
            .done(deferred.resolve)
            .fail(deferred.reject)
            .progress(deferred.notify);
    }

    setTimeout(function() {
        deferred.reject();
    }, time);

    return deferred.promise();
}
like image 177
jgillich Avatar answered Sep 20 '22 15:09

jgillich


I realize this is 2 years old, but in case someone is looking for the answer...

I think Benjamin was close in that you'll want your timeout to be handled separately, so we'll start with his delay function.

function delay(ms){
    var d = $.Deferred();
    setTimeout(function(){ d.resolve(); }, ms);
    return d.promise();
}

Then, if you wanted to wait before code is executed you can call the method you want delayed as a result of this promise.

function timeout(funct, args, time) {
    return delay(time).then(function(){
        // Execute asynchronous code and return its promise
        // instead of the delay promise. Using "when" should
        // ensure it will work for synchronous functions as well.
        return $.when(funct.apply(null, args));
    });
}

This is usually what I'm trying to do when I go looking for a refresher (why I'm here). However, the question was not about delaying the execution, but throwing an error if it took too long. In that case, this complicates things because you don't want to wait around for the timeout if you don't have to, so you can't just wrap the two promises in a "when". Looks like we need another deferred in the mix. (See Wait for the first of multiple jQuery Deferreds to be resolved?)

function timeout(funct, args, time) {
    var d = $.Deferred();

    // Call the potentially async funct and hold onto its promise.
    var functPromise = $.when(funct.apply(null, args));

    // pass the result of the funct to the master defer
    functPromise.always(function(){
        d.resolve(functPromise)
    });

    // reject the master defer if the timeout completes before
    // the functPromise resolves it one way or another
    delay(time).then(function(){
        d.reject('timeout');
    });

    // To make sure the functPromise gets used if it finishes
    // first, use "then" to return the original functPromise.
    return d.then(function(result){
        return result;
    });
}

We can streamline this, knowing that in this case the master defer only rejects if the timeout happens first and only resolves if the functPromise resolves first. Because of this, we don't need to pass the functPromise to the master defer resolve, because it's the only thing that could be passed and we're still in scope.

function timeout(funct, args, time) {
    var d = $.Deferred();

    // Call the potentially async funct and hold onto its promise.
    var functPromise = $.when(funct.apply(null, args))
        .always(d.resolve);

    // reject the master defer if the timeout completes before
    // the functPromise resolves it one way or another
    delay(time).then(function(){
        d.reject('timeout');
    });

    // To make sure the functPromise gets used if it finishes
    // first, use "then" to return the original functPromise.
    return d.then(function(){
        return functPromise;
    });
}
like image 31
Somna Avatar answered Sep 17 '22 15:09

Somna