Reputation: 1439
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:
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.
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
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 return
s 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 .then
s 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