Eitanos30
Eitanos30

Reputation: 1439

Why setTimeout performed parallel if they part of a when chain?

As part of exploring the then chain I encountered two things that I can't explain.

This is the code:

let x1 = new Promise((resolve) => {
  setTimeout(() => {
    console.log('10 seconds first timeout');
    resolve(1);
  }, 10000)
});
let x2 = x1.then((p1) => {
  setTimeout(() => {
    console.log('10 seconds second timeout');
    console.log(p1);
    return ('2');
  }, 10000);
});
let x3 = x2.then((p2) => {
  setTimeout(() => {
    console.log('10 seconds third timeout');
    console.log(p2);
    return ('3');
  }, 10000);
});

console.log(x1, x2, x3);

The problems are:

  1. The x2.then callback prints immediately after the x1.then callback printing. But x2.then gets a callback with a timeout of 10 seconds, so why doesn't this wait 10 seconds before printing? Between the first and the second there is 10 seconsd as expected.

  2. The x1.then callback returns a string '2', as far I understand this value should be the value of the returned Promise (wrapped by Promise), so why when trying to print to console console.log(p2) it prints undefined and not the string '2'?

Upvotes: 1

Views: 83

Answers (1)

ggorlen
ggorlen

Reputation: 56895

x1 looks OK--you've promisified the callback so x2 won't fire until resolve is called. But x2 and x3 don't do what you want because they don't return promises. The returns inside the setTimeout callbacks have no bearing on the mainline promise chain, they just return from the callback block into a scope that ignores the return value.

Given the general x1 pattern, you can put the promisified timer into a function and chain .thens on it to move towards the desired behavior:

const timeout = (cb, ms) => 
  new Promise(resolve => setTimeout(() => resolve(cb()), ms))
;

timeout(() => console.log("a"), 0)
  .then(() => timeout(() => console.log("b"), 1000))
  .then(() => timeout(() => console.log("c"), 1000))
;

console.log("this prints first always because it's synchronous");

As far as the main console.log goes, your approach won't work because you can't turn asynchronous code into synchronous code. All synchronous code has to finish running before JS can begin working on the async task queue. You have to await, then or use a callback to get the values you want, but you can refactor the above code to handle passing values through the resolve call by returning something in the callback and adding a parameter to the next then callback.

const timeout = (cb, ms) => 
  new Promise((resolve, reject) => 
    setTimeout(() => {
      try {
        resolve(cb());
      }
      catch (err) {
        reject(err);
      }
    }, ms)
  )
;

timeout(() => {
    console.log("a");
    return "b";
  }, 0)
  .then(val =>
    timeout(() => {
      console.log(val);
      return "c";
    }, 1000)
  )
  .then(val =>
    timeout(() => console.log(val), 1000)
  )
  .then(() =>
    timeout(() => {
      throw Error("whoops");
    }, 1000)    
  )
  .catch(err => console.error(err.message))
;

As Bergi mentioned in the comments, you need to try/catch or chain the callback off the returned promise instead of passing it in to be able to handle the possibility of callback throws correctly. The above code illustrates this by calling reject in the promise.

Bergi also suggests that returning values from timeouts is an antipattern. You can delay execution of the next promise without a return with something like:

const wait = ms => 
  new Promise(resolve => setTimeout(resolve, ms))
;

Promise
  .resolve()
  .then(() => {
    console.log("a");
    return wait(1000);
  })
  .then(() => {
    console.log("b");
    return wait(1000);
  })
  .then(() => {
    console.log("c");
  })
  .catch(err => console.error(err))
;

Either way, for this use case, async/await should be cleaner, especially if you want to pass values along the promise chain:

const wait = ms => 
  new Promise(resolve => setTimeout(resolve, ms))
;

(async () => {
  console.log("a");
  await wait(1000);
  console.log("b");
  await wait(1000);
  console.log("c");
})();

Upvotes: 1

Related Questions