Asynchronicity Flashcards
Before promises were introduced how does one do asynchronous programming
Using callbacks.
The only way to guarantee that a sync function is called after an async function call is called was to have the async function take in a callback which is calls after it has finished executing.
E.g.
function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); document.head.append(script); }
Similarly, if we need to make multiple asynchronous calls, but guarantee they are executed sequentially, we define each of them as a callback:
loadScript('/my/script.js', function(script) { loadScript('/my/script2.js', function(script) { loadScript('/my/script3.js', function(script) { // ...continue after all scripts are loaded }); }); });
One issue with this approach is ‘callback hell’ - if you have N callbacks then there will be N levels of nesting.
Another issue is handling errors. Suppose we update loadScript to:
function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`Script load error for ${src}`)); document.head.append(script); }
Then now we would have:
loadScript('1.js', function(error, script) { if (error) { handleError(error); } else { // ... loadScript('2.js', function(error, script) { if (error) { handleError(error); } else { // ... loadScript('3.js', function(error, script) { if (error) { handleError(error); } else { // ...continue after all scripts are loaded (*) } }); } }); } });
We could improve it like below, but it’s bad developer experience both for the author and the reader.
loadScript('1.js', step1); function step1(error, script) { if (error) { handleError(error); } else { // ... loadScript('2.js', step2); } } function step2(error, script) { if (error) { handleError(error); } else { // ... loadScript('3.js', step3); } } function step3(error, script) { if (error) { handleError(error); } else { // ...continue after all scripts are loaded (*) } }
What is a Promise?
A Promise is an object produced by calling the Promise constructor function that takes in an executor function. When the promise is created, the executor function is run immediately. The executor function has resolve and reject callbacks which it should call when a result or error is produced.
Thus, the promise object { state: ‘pending’ | ‘fulfilled’ | ‘rejected’, result: any } is initially in ‘pending’.
If resolve() is called then it is ‘fulfilled’ and result is the value passed into resolve().
If reject() is called then it is ‘rejected’ and result is the value passed into reject().
Note that any state changes(i.e. resolve() or reject() calls) are final; only the first one is considered.
Note that state and result are hidden properties. We can access result by using .then and .catch
For example:
fetch(‘/article/promise-chaining/user.json’)
.then(response => response.json())
.then(user => fetch(https://api.github.com/users/${user.name}
))
.then(response => response.json())
.then(githubUser => {…});
.catch(e) { alert(e.message) }
A good rule is that async code should always be wrapped in a promise. This is for forward compatibility, if we do add any code in the future, the behavior is still predictable.
What’s the difference between:
promise
.then(f1)
.catch(f2)
and
promise
.then(f1, f2);
In the second example, f2 is called when the promise is rejected.
In the first example, f2 is called when the promise is rejected or f1 throws an error.
What’s the difference between:
new Promise((resolve, reject) => { throw new Error("Whoops!"); }).catch(alert); new Promise((resolve, reject) => { reject(new Error("Whoops!")); }).catch(alert);
No difference. There’s an implicit try-catch wrapper around executor functions that rejects any errors thrown.
Is the following code valid? If so, when would you do this?
.catch((error) => {
})
.then((result) => {
});
Yes, it is perfectly valid.
The you might do this if instead of having a single .catch for all errors, you want to handle an error at different stages of the promise chain.
What is Promise.all() and when is it used? What about Promise.allSettled?
Promise.all takes in an iterable of Promises, and returns a new Promise that resolves when all of the promises resolve, otherwise reject if any of the promises are rejected.
Note:
- There’s no concept of cancellation, so although Promise.all() might reject, the promises are still executing in the background.
- You can pass in non-promise values in the array, they will be returned as-is
Promise.allSettled is preferred when you don’t want to reject eagerly and want all the promises to execute. It returns an array of:
- {status:”fulfilled”, value:result} for successful responses,
- {status:”rejected”, reason:error} for errors.
What is Promise.race? When might it be used?
What about Promise.any, how is it different?
Promise.race() takes in an iterable of promises and returns the first one that settles(finishes).
Some use cases:
1. Timeout to stop waiting for api call
const fetchData = fetch(‘https://example.com/data’);
const timeout = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(‘Request timed out’));
}, 5000);
});
Promise.any() is similar to Promise.race() except it returns the first promise that resolves(not settled). If all promises are rejected, it returns a n AggregrateError.
What is Promisify?
It is a common utility function to convert callback-based asynchronous functions, into promise-based asynchronous functions.
import { readFileSync } from 'fs' //readFile(path, callback) function promisify(_function) { return function(...args) { return new Promise((resolve, reject) => { _function(...args, (error, result) => { if(error) reject(error); resolve(result) }) }) } }
What is the output of the following code?
let promise1 = Promise.resolve(); let promise2 = Promise.resolve(); promise1 .then(() => console.log(1)) .then(() => console.log(2)); promise2 .then(() => console.log(3)) .then(() => console.log(4))
1,3,2,4
Promise handlers are asynchronous. When a promise is ready, its .then/catch/finally handlers are put into the microtask queue; they are not executed yet. When the JavaScript engine becomes free from the current code, it takes a task from the queue and executes it.
promise 1 and promise 2 have console.log(1) and console.log(3) event handlers directly attached, so these two go into the event queue. aAfter the global code is done executing, console.log(1) handler is first brought back to the call stack to be executed. after it is done, it returns a promise whose handler is console.log(2) goes to the event queue for now. Next, the console.log(3) is brought back from the event queue, and it also returns a promise whose handler i.e. the console.log(4) is stored in the event queue.