Javascript: Promises and mistakes
Javascript: Promises and mistakes
Hi, today I'm gonna talk about Javascript Promises, Async-await, and some mistakes which I made when working with them.
When I write a blog, I don't want to repeat things that are shared many times before (like what's promise in Javascript, when to use them, what's async-await for,....) so I will focus on some notes, mistakes which I've been through in the past that made me feel like I'm stupid. Okay, so let's start by now
One other thing is that I will come back to update this post whenever I find out any new mistake.
Hmm, it might be not really related to our topic today, but actually, it is. This might rarely happen (actually for the past 2 years, I've just encountered it several times).
console.* is not standardized and there is no specification or rules which show how console.* methods works. So we can't know for sure the behavior of console.*. Their results can be changed from time to time, the result of the example I'm going to show might not be the same as the result you see on your browser at the time you read this blog. We never know.
Let's see a very simple example:
const a = {
value: 10,
};
console.log(`a`, a);
a.value = 20;const a = {
value: 10,
};
console.log(`a`, a);
a.value = 20;What you guys might expect to see is:
{value: 10}{value: 10}and it actually is, but I want to point out that, SOMETIME, it will log like this:
{value: 20}{value: 20}It's hard for me to reproduce a case to show you guys but it really can happen, maybe when you run a very large project and rapidly make changes to code then the console.* will have some strange behavior that you never know.
If you run into the above scenario, the best option you can do is that use a debugger instead or stick with JSON.stringify to take a "snapshot" of your variable (I'm talking about object type)
One mistake that I made during development is that I assumed resolve/reject is the "return" keyword of a Promise. Technically, it's true, but one thing to notice is that even if you called resolve/reject, your Promise will not stop at that point but will exec till the end of the Promise.
Let's see this example:
const a = new Promise((resolve) => {
resolve(1);
console.log("still go here");
});
a.then((res) => {
console.log(res);
});
// Result
still go here
1const a = new Promise((resolve) => {
resolve(1);
console.log("still go here");
});
a.then((res) => {
console.log(res);
});
// Result
still go here
1And
const a = new Promise((resolve, reject) => {
reject(2);
console.log("still go here");
});
a.then((res) => {
console.log(res);
}).catch((err) => {
console.log(err);
});
// Result
still go here
2const a = new Promise((resolve, reject) => {
reject(2);
console.log("still go here");
});
a.then((res) => {
console.log(res);
}).catch((err) => {
console.log(err);
});
// Result
still go here
2Even though you called resolve/reject before the console.log, it's still printed out on screen which means Promise still go to that line and execute code after resolve/reject has been called.
It's not the same as functions which if you call "return', the function will stop at that point (I'm not talking about conditional return).
I want to share this with you guys because in the past, because of assuming resolve/reject as "return", I made a bug that took me 2 days to fix!
In your promise, you can call resolve/reject multiple times, but only the first one will be accepted and be the output of your Promise. Of course, I'm talking about the normal flow, if it's conditional flow, it will be a different story.
const a = new Promise((resolve) => {
resolve(1);
resolve(2);
});
a.then(console.log);
// Output:
1const a = new Promise((resolve) => {
resolve(1);
resolve(2);
});
a.then(console.log);
// Output:
1const a = new Promise((resolve, reject) => {
reject(2);
reject(1);
});
a.catch(console.log);
**// Output
2**const a = new Promise((resolve, reject) => {
reject(2);
reject(1);
});
a.catch(console.log);
**// Output
2**The same thing happens with the parameters you pass to resolve/reject, you can call resolve/reject with multiple parameters, but only the first one will be chosen to be the value of resolve/reject, all subsequent parameters beyond the first one will be silent ignore.
const a = new Promise((resolve, reject) => {
resolve(1, 2, 3);
});
a.then(console.log);
// Ouput
1const a = new Promise((resolve, reject) => {
resolve(1, 2, 3);
});
a.then(console.log);
// Ouput
1If you want to return more than 1 value, you have no choice but put them into object or array.
const a = new Promise((resolve, reject) => {
resolve([1, 2, 3]);
});
a.then(console.log);
// Ouput
[1,2,3]const a = new Promise((resolve, reject) => {
resolve([1, 2, 3]);
});
a.then(console.log);
// Ouput
[1,2,3]Okay, let me make it more clear.
Consider below example
const promise = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve("done");
}, 1500);
});
const makeRequest = async () => {
return await promise();
};
const test = async () => {
const result = await makeRequest();
console.log(result);
};
test();
// result
// after 1.5s, print out "done"const promise = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve("done");
}, 1500);
});
const makeRequest = async () => {
return await promise();
};
const test = async () => {
const result = await makeRequest();
console.log(result);
};
test();
// result
// after 1.5s, print out "done"Notice about the return await promise() . I think not only me but many other beginners sometimes write such code like that. It's not wrong, but to me, it's kind of useless. Since make request might make some asynchronous request to the server and return another promise. if we don't have to process anything with the data but return it immediately, we don't have to await the response before return
This code also works the same as the above:
const promise = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve("done");
}, 1500);
});
const makeRequest = async () => {
return promise();
};
const test = async () => {
const result = await makeRequest();
console.log(result);
};
test();
// Result
// after 1.5s, print out "done"const promise = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve("done");
}, 1500);
});
const makeRequest = async () => {
return promise();
};
const test = async () => {
const result = await makeRequest();
console.log(result);
};
test();
// Result
// after 1.5s, print out "done"One of the drawback of Promise.all is that it will stop as soon as one of the promise is rejected and you have no idea about how many promises is rejected but only the first one (I assume that you do have a catch block to determine which one was failed). Sometimes it’s good, but in other cases it doesn't, especially you want to know which one is failed to execute a retry for it but still want to await for multi promises, the solution for this case would be using Promise.allSettled.
The most significant difference between Promies.allSettled and Promise.all is that Promise.allSettel will wait for all promises to be resolved/reject while Promise.all will stop as soon as one of them failed. The result for Promise.allSettled is an array with status and result of the promise, like this
[
{
"status": "fulfilled",
"value": [value]
},
{
"status": "rejected",
"reason": [error]
}
][
{
"status": "fulfilled",
"value": [value]
},
{
"status": "rejected",
"reason": [error]
}
]One common mistake about promise.all is that people tends to explain Promise.all as “execute multi promises in parallel” which is not correct. Promises can not be “executed", they start their task as soon as it’s created and they only hold their result. When you use Promise.all just awaits for multi promises to be resolved/rejected.
Notice about the highlighted sentence above. Another common mistake is that people usually assume that whenever you use await, they will run in sequence. But the truth is not. Let's see an example
(async () => {
const async500ms = () => {
return new Promise((resolve) => {
setTimeout(resolve, 500, "correct500msResult");
});
};
const async100ms = () => {
return new Promise((resolve) => {
setTimeout(resolve, 100, "correct100msResult");
});
};
const test = async () => {
const label = "test run in";
console.time(label);
const p1 = async500ms();
const p2 = async100ms();
const result = [await p1, await p2];
console.log("result", result);
console.timeEnd(label);
};
test();
})();
// result
result [ 'correct500msResult', 'correct100msResult' ]
test run in: 510.086ms(async () => {
const async500ms = () => {
return new Promise((resolve) => {
setTimeout(resolve, 500, "correct500msResult");
});
};
const async100ms = () => {
return new Promise((resolve) => {
setTimeout(resolve, 100, "correct100msResult");
});
};
const test = async () => {
const label = "test run in";
console.time(label);
const p1 = async500ms();
const p2 = async100ms();
const result = [await p1, await p2];
console.log("result", result);
console.timeEnd(label);
};
test();
})();
// result
result [ 'correct500msResult', 'correct100msResult' ]
test run in: 510.086msWhy test run in ~500ms while we used await?
That’s because async500ms and async100ms run right after when we call it. and when we use await, yeah, it does run in sequence, but because the tasks of async500ms and async100s have already ran and when we await for the results of these promises, we only have to wait for the longest one to be resolved (because other promises are resolved before it). It’s just like you make a request for a party table with 10 dishes and say that: “I want you to serve me a table with all the things that I’ve ordered, not one by one. So at that time, the waitress will wait for all of dishes to be done (even though there are some dishes which are ready sooner) and the dishes are cook as soon as the order is placed.