20

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?

mnowotka
  • 16,430
  • 18
  • 88
  • 134
  • Your primitives are wrong, you need to promisify it in two stages, first - promisify the superImportantLibrary.doSomething method and only then perform the promise return. Also, please avoid jQuery promises, they are horrible compared to other implementations. – Benjamin Gruenbaum Jun 12 '14 at 14:30
  • @BenjaminGruenbaum - Which implementations? Why jQuery promises are horrible? What do you mean by saying 'your primitives are wrong'? How can I 'promisify' `superImportantLibrary.doSomething` if it's library and not my own code, can you write some example code to explain what do you mean by that? – mnowotka Jun 12 '14 at 14:58
  • 1
    I was afraid I wouldn't be able to make such claims without having to justify myself :) So [this is how to convert an API to promises](http://stackoverflow.com/questions/22519784/how-do-i-convert-an-existing-callback-api-to-promises) (convert the library itself), [this is why jQuery deferreds are bad](http://stackoverflow.com/questions/23744612/problems-inherent-to-jquery-deferred/23744774#23744774) and [as explained by domenic](https://gist.github.com/domenic/3889970), as for the library, I'd use [Bluebird](https://github.com/petkaantonov/bluebird) – Benjamin Gruenbaum Jun 12 '14 at 15:04
  • @BenjaminGruenbaum - so `myFunc` is a way to promisify `superImportantLibrary.doSomething` method. And it returns only a promise. Why do you say it's wrong? I would still appreciate some code explaining how would you do this in the correct way. Thanks for other links! – mnowotka Jun 12 '14 at 15:13
  • I've added an answer, if you're unsure about anything please feel free to ask for clarifications. – Benjamin Gruenbaum Jun 12 '14 at 15:30
  • can't you just set a timer and the asynchronous function to both resolve the promise? +1 on the question, gonna do my homework now. – Shanimal Jun 12 '14 at 15:33
  • @Shanimal - again I'd appreciate some code which would better explain what you mean. – mnowotka Jun 12 '14 at 15:46

3 Answers3

9
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();
}
jgillich
  • 71,459
  • 6
  • 57
  • 85
  • Nope. You can't call `reject` or `resolve` on function that returns promise. Sorry I can't change `myFunc` to return deffered instead of promise... – mnowotka Jun 12 '14 at 12:08
  • Ah sorry I missed that you had just a promise. Try the above again, I guess proxying all callback should work. – jgillich Jun 12 '14 at 12:22
  • 1
    I think you can skip the `if(deferred.state() === 'pending')` because `deferred.reject()` will do nothing if the deferred is already rejected or resolved. – jfriend00 Jun 12 '14 at 22:02
  • @jfriend00 well, I tried to fix this answer, to avoid confusing others on the basic function promises, but 3/5 who reviewed it don't understand promises/deferred, I guess. **At ANY point when `setTimeout` triggers, the deferred is necessarily Pending, Rejected, Resolved. Once a deferred is Rejected or Resolved, it cannot change.** WRT the review (http://j.mp/1Jq5eOB): The code's intent is to `reject` when the timeout elapses, **period**. So, the edit wasn't addressing the author, nor changing the original intent of the answer: See jQuery docs (http://j.mp/1LbRORT ) paragraph 3, sentence 4. – JoeBrockhaus Feb 19 '15 at 17:07
  • 1
    @JoeBrockhaus - well, the reviewers were just wrong (which happens from time to time). Removing the `if(deferred.state() === 'pending')` just removes dead code and does not change the functionality in any way because `deferred.reject()` already has its own check internally and won't do anything if the state isn't `pending` (per the promises specification). Oh well. – jfriend00 Feb 19 '15 at 22:27
  • Indeed. Just wanted to document the attempt to fix it. Don't feel like starting a flame war by submitting again lol – JoeBrockhaus Feb 19 '15 at 23:47
  • 1
    Is `new` required when calling `$.Deferred()`? I hadn't been doing it. – Danyal Aytekin Jul 26 '16 at 09:35
5

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;
    });
}
Somna
  • 148
  • 2
  • 9
3

You should always promsiify at the lowest level possible. Let's start from the basics.

I'll use jQuery promises here, but this should really be done with a stronger library like Bluebird Let's start simple, by creating our delay as:

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

Note delay doesn't do anything surprising, all our delay function does is cause a delay of ms milliseconds.

Now, for your library, we want to create a version of doSomething that works with promises:

 superImportantLibrary.doSomethingAsync = function(){
     var d = $.Deferred();
     superImportantLibrary.doSomething(function(data){ d.resolve(data); });
     return d.promise();
 };

Note both our delay and doSomethingAsync functions both do just one thing. Now the fun begins.

function timeout(promise,ms){
    var timeout = delay(ms); // your timeout
    var d = $.Deferred();
    timeout.then(function(){ d.reject(new Error("Timed Out")); });
    promise.then(function(data){ d.resolve(data); });
    return d.promise();
}

timeout(superImportantLibrary.doSomethingAsync(),1000).then(function(data){
     // handle success of call
}, function(err){
     // handle timeout or API failure.
});

Now in Bluebird, this whole code would have been:

superImportantLibrary.doSomethingAsync().timeout(1000).then(function(){
    // complete and did not time out.
});
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Yes, but you could as well write `timeout(myFunc(), 1000).then(...)` so I don't really understand why you insist on replacing it by `doSomethingAsync`. Apart from that, in the `promise.then(...)` part, there should be code for handling errors, and progress as well but I understand that you left it as an implementation detail. – mnowotka Jun 12 '14 at 15:44
  • What if `promise` is rejected? What about progress notifications? I sense a deferred antipattern :-) Cleanest solution would probably be something like `Promise.race(promise, rejectAfterDelay(ms))`. – Bergi Jun 12 '14 at 17:06
  • @Bergi yes definitely, cleanest solution would be what I said latest in Bluebird. The problem is jQuery doesn't have a `.race` method or anything similar so implementing it would be very ugly. – Benjamin Gruenbaum Jun 12 '14 at 17:27
  • `doSomethingAsync` just wraps the library function, _not_ your validation checks on the length etc. Remember, promises are throw safe. – Benjamin Gruenbaum Jun 12 '14 at 20:28