37

I am trying to run a test suite for an object that returns a promise. I want to chain several actions together with short timeouts between them. I thought that a "then" call which returned a promise would wait for the promise to be fulfilled before firing the next chained then call.

I created a function

function promiseTimeout (time) {
  return new Promise(function(resolve,reject){
    setTimeout(function(){resolve(time);},time);
  });
};

to try and wrap setTimeout in a Promise.

Then in my test suite, I am calling something like this ...

    it('should restore state when browser back button is used',function(done){
      r.domOK().then(function(){
        xh.fire('akc-route-change','/user/4/profile/new');
      }).then(promiseTimeout(2000)).then(function(t){
        xu.fire('akc-route-change','/user/6');
      }).then(promiseTimeout(10)).then(function(t){
        expect(xu.params[0]).to.equal(6);
        history.back();
      }).then(promiseTimeout(10)).then(function(){
        expect(xu.params[0]).to.equal(4);
        done();
      });
    });

I can put a breakpoint on the first xh.fire call and a second one on the xu.fire call and would have expected a two second gap when a continues from the first breakpoint to the second.

Instead it reaches the second breakpoint immediately, and the value of t at that point is undefined.

What am I doing wrong?

akc42
  • 4,893
  • 5
  • 41
  • 60

5 Answers5

29

TL;DR - you've wrapped setTimeout in a promise properly, the issue is you are using it improperly

.then(promiseTimeout(2000)).then

will not do what you expect. The "signature" for .then is then(functionResolved, functionRejected)

A promise’s then method accepts two arguments:

promise.then(onFulfilled, onRejected)

Both onFulfilled and onRejected are optional arguments:

  • If onFulfilled is not a function, it must be ignored.
  • If onRejected is not a function, it must be ignored.

source: https://promisesaplus.com/#point-21

You are not passing a function to then

Consider the way you are doing it:

Promise.resolve('hello')
.then(promiseTimeout(2000))
.then(console.log.bind(console))

vs how it should be done:

Promise.resolve('hello').then(function() { 
    return promiseTimeout(2000)
}).then(console.log.bind(console))

The first outputs 'hello' immediately

The second outputs 2000 after 2 seconds

Therefore, you should be doing:

it('should restore state when browser back button is used', function(done) {
    r.domOK().then(function() {
        xh.fire('akc-route-change', '/user/4/profile/new');
    }).then(function() {
        return promiseTimeout(2000);
    }).then(function(t) {
        xu.fire('akc-route-change', '/user/6');
    }).then(function() {
        return promiseTimeout(10);
    }).then(function(t) {
        expect(xu.params[0]).to.equal(6);
        history.back();
    }).then(function() {
        return promiseTimeout(10);
    }).then(function() {
        expect(xu.params[0]).to.equal(4);
        done();
    });
});

Alternatively:

it('should restore state when browser back button is used', function(done) {
    r.domOK().then(function() {
        xh.fire('akc-route-change', '/user/4/profile/new');
    }).then(promiseTimeout.bind(null, 2000)
    ).then(function(t) {
        xu.fire('akc-route-change', '/user/6');
    }).then(promiseTimeout.bind(null, 10)
    ).then(function(t) {
        expect(xu.params[0]).to.equal(6);
        history.back();
    }).then(promiseTimeout.bind(null, 10)
    ).then(function() {
        expect(xu.params[0]).to.equal(4);
        done();
    });
});

EDIT: March 2019

Over the years, things have changed a lot - arrow notation makes this even easier

Firstly, I would define promiseTimeout differently

const promiseTimeout = time => () => new Promise(resolve => setTimeout(resolve, time, time));

The above returns a function that can be called to create a "promise delay" and resolves to the time (length of delay). Thinking about this, I can't see why that would be very useful, rather I'd:

const promiseTimeout = time => result => new Promise(resolve => setTimeout(resolve, time, result));

The above would resolve to the result of the previous promise (far more useful)

But it's a function that returns a function, so the rest of the ORIGINAL code could be left unchanged. The thing about the original code, however, is that no values are needed to be passed down the .then chain, so, even simpler

const promiseTimeout = time => () => new Promise(resolve => setTimeout(resolve, time));

and the original code in the question's it block can now be used unchanged

it('should restore state when browser back button is used',function(done){
  r.domOK().then(function(){
    xh.fire('akc-route-change','/user/4/profile/new');
  }).then(promiseTimeout(2000)).then(function(){
    xu.fire('akc-route-change','/user/6');
  }).then(promiseTimeout(10)).then(function(){
    expect(xu.params[0]).to.equal(6);
    history.back();
  }).then(promiseTimeout(10)).then(function(){
    expect(xu.params[0]).to.equal(4);
    done();
  });
});
Jaromanda X
  • 53,868
  • 5
  • 73
  • 87
  • Got it. Like the second approach, although I just tried both and both work I also tidied up promiseTimeout - like the comment made in response to Benjamin Gruenbaum – akc42 Nov 21 '15 at 12:45
  • Yes, this works fine, but minor point, in the first code fragment, doing `function() { return promise.Timeout(n); }` over and over again seems excessively verbose, would like to just write `makePromiseTimeout(n)` instead. –  Nov 21 '15 at 15:58
  • Same issue [worth reading here](https://stackoverflow.com/a/38956175/444255), with a bit more ES6-Syntax and /w and w/o original promise pass-through. – Frank N Apr 17 '18 at 04:41
8

To make a timeout which works as you want, write a function which takes a delay, and returns a function suitable for passing to then.

function timeout(ms) {
  return () => new Promise(resolve => setTimeout(resolve, ms));
}

Use it like this:

Promise.resolve() . then(timeout(1000)) . then(() => console.log("got here"););

However, it is likely that you will want to access the resolved value of the promise leading into the timeout. In that case, arrange for the function created by timeout() to pass through the value:

function timeout(ms) {
  return value => new Promise(resolve => setTimeout(() => resolve(value), ms));
}

Use it like this:

Promise.resolve(42) . then(timeout(1000)) . then(value => console.log(value));
  • I don't understand this answer. Taking away the new ES6 notation, and renaming timeout to promiseTimeout the first part of your answer seems identical to my original question which doesn't work. Where is the difference? – akc42 Nov 21 '15 at 14:18
  • My `timeout` function returns a function which returns a promise. Your original one returns a promise. `then` requires a function--passing it a promise as you were doing will do nothing at all. –  Nov 21 '15 at 15:43
  • 1
    Sorry for previous comment - I now understand what you are saying. I find this new ES6 notation a bit difficult to understand without careful thought and effectively translating it back to the Javascript I understand. – akc42 Nov 24 '15 at 21:46
  • This is very neat but in my code, the test returns success before the timeout fires and the actual test runs. Test failure generates an error. I changed it to: 'return Promise.resolve(42).then...' and now the test correctly reports success / failure. – Little Brain Apr 08 '19 at 17:00
  • Why is the first `Promise.resolve().then` needed? Why not just let your `timeout` return `new Promise(...)`? – Eric Nov 26 '19 at 09:35
7

This has already been answered above, but I feel this could be done easily with:

const setTimeoutPromise = ms => new Promise(resolve => setTimeout(resolve, ms))

setTimeoutProise function accept wait time in ms and passes it down to the setTimeout function. Once the wait time is over, the resolve method passed down to the promise is executed.

Which could be used like this:

setTimeoutPromise(3000).then(doSomething)
Phil
  • 597
  • 11
  • 26
4
await new Promise((resolve, reject)=>{
    // wait for 50 ms.
    setTimeout(function(){resolve()}, 50);
});
console.log("This will appear after waiting for 50 ms");

This can be used in an async function and the execution will wait till given interval.

ashish
  • 425
  • 4
  • 11
  • 2
    While this code may answer the question, providing additional context regarding **how** and **why** it solves the problem would improve the answer's long-term value. – Alexander Mar 21 '18 at 13:44
  • This is great for doing the await style fetch chains, so if you drop this above your const res = await fetch( someURL ) it delays it like a charm, no extra fluff. – Lux.Capacitor Apr 06 '18 at 19:38
0

Another approach for adding delays to Promises without having to predefine or import a helper function (that I personally like best) is to extend the property of the Promise constructor:

Promise.prototype.delay = function (ms) {
  return new Promise(resolve => {
    window.setTimeout(this.then.bind(this, resolve), ms);
  });
}

I'm leaving out the reject callback since this is meant to always resolve.

DEMO

Promise.prototype.delay = function(ms) {
  console.log(`[log] Fetching data in ${ms / 1000} second(s)...`);

  return new Promise(resolve => {
    window.setTimeout(this.then.bind(this, resolve), ms);
  });
}

document.getElementById('fetch').onclick = function() {
  const duration = 1000 * document.querySelector('#duration[type="number"]').value;

  // Promise.resolve() returns a Promise
  // and this simply simulates some long-running background processes 
  // so we can add some delays on it
  Promise
    .resolve('Some data from server.')
    .delay(duration)
    .then(console.log);
}
<div>
  <input id="duration" type="number" value="3" />
  <button id="fetch">Fetch data from server</button>
</div>

Or if you need it to also be .catch()-able, here is when you need the second argument (reject). Note that the catch() handling will also occur after the delay:

Promise.prototype.delay = function(ms) {
  console.log(`[log] Fetching data in ${ms / 1000} second(s)...`);

  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      this.then(resolve).catch(reject);
    }, ms);
  });
}

document.getElementById('fetch').onclick = function() {
  const duration = 1000 * document.querySelector('#duration[type="number"]').value;

  Promise
    .reject('Some data from server.')
    .delay(duration)
    .then(console.log)
    .catch(console.log); // Promise rejection or failures will always end up here
}
<div>
  <input id="duration" type="number" value="3" />
  <button id="fetch">Fetch data from server</button>
</div>
Yom T.
  • 8,760
  • 2
  • 32
  • 49