Tuesday, September 16, 2025

"Understanding JavaScript Promises" Nicholas C. Zakas

 



Best book notes:

“Each promise goes through a short lifecycle starting in the pending state, which indicates that promise hasn’t completed yet. A pending promise is considered unsettled. The promise in the previous example is in the pending state as soon as the fetch() function returns it. Once the promise completes, the promise is considered settled and enters one of two possible states (see Figure 1-1): 1. Fulfilled: The promise has completed successfully. 2. Rejected: The promise didn’t complete successfully due to either an error or some other cause.”

“An internal [[PromiseState]] property is set to “pending”, “fulfilled”, or “rejected” to reflect the promise’s state. This property isn’t exposed on promise objects, so you can’t determine which state the promise is in programmatically. But you can take a specific action when a promise changes state by using the then() method.”

“Promises also have a catch() method that behaves the same as then() when only a rejection handler is passed.”

“To go along with then() and catch() there is also finally(). The callback passed to finally() (called a settlement handler) is called regardless of success or failure. Unlike the callbacks for then() and catch(), finally() callbacks do not receive any arguments because it isn’t clear whether the promise was fulfilled or rejected”

“JavaScript executed in the regular flow of a program is executed as a task, which is to say that the JavaScript runtime has created a new execution context and executes the code completely, exiting when finished. As an example, an onclick handler for a button in a web page is executed as a task. When the button is clicked, a new task is created and the onclick handler is executed. Once complete, the JavaScript runtime waits for the next interaction to execute more code. Promise handlers, however, are executed in a different way.”

“All promise handlers, whether fulfillment, rejection, or settlement, are executed as microtasks inside of the JavaScript engine. Microtasks are queued and then executed immediately after the currently running task has completed, before the JavaScript runtime becomes idle. Calling then(), catch(), or finally() tells a promise to queue the specified microtasks once the promise is settled. This is different than creating timers using setTimeout() or setInterval(), both of which create new tasks to be executed at a later point in time. Queued promise handlers will always execute before timers that are queued in the same task”

“New promises are created using the Promise constructor. This constructor accepts a single argument: a function called the executor, which contains the code to initialize the promise. The executor is passed two functions named resolve() and reject() as arguments. You call the resolve() function when the executor has finished successfully to signal that the promise is resolved or the reject() function to indicate that the operation has failed.”

“A promise can only be resolved once, so if you call resolve() more than once inside of an executor, every call after the first is ignored.”

“The Promise constructor is useful for creating new promises when you can easily encapsulate settlement behavior inside of an executor. However, there may be times when you want to create a promise and then define its settlement behavior later. You can create a deferred promise using Promise.withResolvers() where the settlement behavior occurs after the creation of the promise rather than during its creation. The Promise.withResolvers() method returns an object with the following proper- ties: 1) promise - an instance of Promise 2) resolve - a function to call to resolve promise 3) reject - a function to call to reject promise”

“The Promise.resolve() method accepts a single argument and returns a promise in the fulfilled state. That means you don’t have to supply an executor if you know the value of the promise already.”

“You can also create rejected promises by using the Promise.reject() method. This works like Promise.resolve() except the created promise is in the rejected state”

“Both Promise.resolve() and Promise.reject() also accept non-promise thenables as arguments. When passed a non-promise thenable, these methods create a new promise that is settled with the same value and state of the settled thenable. A non-promise thenable is created when an object has a then() method that accepts a resolve and a reject argument”

“A promise is a placeholder for a value that may be provided later as the result of some asynchronous operation. Instead of assigning an event handler or passing a callback into a function, you can use a promise to represent the result of an operation. Promises have three states: pending, fulfilled, and rejected. A promise starts in a pending (unsettled) state and becomes fulfilled on a successful execution or rejected on a failure (fulfillment and rejection are settled states). In either case, handlers can be added to indicate when a promise is settled. The then() method allows you to assign a fulfillment and rejection handler; the catch() method allows you to assign only a rejection handler; the finally() method allows you to assign a settlement handler that is always called regardless of the outcome. All promise handlers are run as microtasks so they will not execute until the current script job is complete.”

“Each call to then(), catch(), or finally() actually creates and returns another promise. This second promise is settled only once the first has been fulfilled or rejected.”

“Always have a rejection handler at the end of a promise chain to ensure that you can properly handle any errors that may occur.”

“Another important aspect of promise chains is the ability to pass data from one promise to the next. You’ve already seen that a value passed to the resolve() handler inside an executor is passed to the fulfillment handler for that promise. You can continue passing data along a chain by specifying a return value from the fulfillment handler.”

“Multiple promises can be chained together in a variety of ways to pass information between them. Each call to then(), catch(), and finally() creates and returns a new promise that is resolved when the preceding promise is settled. If the promise handler returns a value, then that value becomes the value of the newly created promise from then() and catch() (finally() ignores this value); if the promise handler throws an error, then the error is caught and the returned newly created promise is rejected using that error as the reason. When one promise is rejected in a chain, the promises created from other chained handlers are also rejected until the end of the chain is reached. Knowing this, it’s recommended to attach a rejection handler at the end of each promise chain to ensure that errors are handled correctly. Failing to catch a promise rejection will result in a message being output to the console, an error being thrown, or both (depending on the runtime environment).”

“The Promise.all() method accepts a single argument, which is an iterable (such as an array) of promises to monitor, and returns a promise that is resolved only when every promise in the iterable is resolved.”

“You’ll want to use Promise.all() in any situation where you are waiting for multiple promises to fulfill, and any one failure should cause the entire operation to fail.”

“The Promise.allSettled() method is a slight variation of Promise.all() where the method waits until all promises in the specified iterable are settled, regardless of whether they are fulfilled or rejected. The return value of Promise.allSettled() is always a promise that is fulfilled with an array of result objects.”

“The result object for a fulfilled promise has two properties: 1) status - always set to the string fulfilled 2) value - the fulfillment value of the promise”

“For a rejected promise, there are also two properties on the result object: 1) status - always set to the string rejected 2) reason - the rejection value of the promise”

“The Promise.allSettled() method can be used in a lot of the same situations as Promise.all(); however, it is best suited for when you want to ignore rejections, handle rejections differently, or allow partial success. Here are some common use cases for Promise.allSettled().”

“The Promise.any() method also accepts an iterable of promises and returns a fulfilled promise when any of the passed-in promises are fulfilled. The operation short-circuits as soon as one of the promises is fulfilled.”

“Promise.any() receives promises that are not fulfilled, and so the returned promise is rejected with an AggregateError. You can inspect the errors property, which is an array, to retrieve the rejection values from each promise”

“The Promise.any() method is best used in situations where you want any one of the promises to fulfill and you don’t care how many others reject unless they all reject.”

“The Promise.race() method provides a slightly different take on monitoring multiple promises. This method also accepts an iterable of promises to monitor and returns a promise, but the returned promise is settled as soon as the first promise is settled. Instead of waiting for all promises to be resolved like the Promise.all() method or short-circuiting only for the first resolved promise like Promise.any(), the Promise.race() method returns an appropriate promise as soon as any promise in the array is settled.”

“The Promise.race() method is best used in situations where you want to be able to short-circuit the completion of a number of different promises. Unlike Promise.any(), where you specifically want one of the promises to succeed and only care if all promises fail, with Promise.race() you want to know even if one promise fails as long as it fails before any other promise fulfills.”

“The async keyword indicates that the following function or method should be made asynchronous. It’s important for the JavaScript engine to know ahead of time if a function is asynchronous because it behaves differently than a synchronous function.”

“Async functions are different from synchronous functions in four ways: 1. The return value is always a promise 2. Thrown errors are promise rejections 3. The await expression can be used 4. The for-await-of loop can be used”

“The await expression is designed to make working with promises simple. Instead of manually assigning fulfillment and rejection handlers, any promise used in an await expression behaves more like code in a synchronous function: the expression returns the fulfilled value of a promise when it succeeds and throws the rejection value when the promise fails. That allows you to easily assign the result of an await expression to a variable and catch any rejections using a try-catch statement.”

“You can also use await at the top level of a JavaScript module outside of an async function. Essentially, a JavaScript module acts as an async function wrapped around the entire module by default. This allows you to call promise-based functions directly, such as using the import() function”

“Using top-level await, you can load modules dynamically alongside the statically loaded modules. (Dynamically loaded modules allow you to construct the module specifier dynamically, as well, which is not possible with static import.)”

“The AbortController class is not defined in ECMA-262, but rather in the DOM Standard⁴. Even so, it has become the standard for cancelling promise-based requests across all JavaScript runtimes. An AbortController object has only two members: 1. abort() - the method to call when you want to abort the request 2. signal - an instance of AbortSignal that encapsulates abort notifications”

“When controller.abort() is called, the promise returned from fetch() is rejected with an error whose name property is “AbortError”. It’s important to check the name property to distinguish abort errors from other types of errors.”

AbortSignal static methods are meant to aid in creating a variety of abort conditions for abortable functions: 1) AbortSignal.timeout() - creates a signal that aborts after an amount of time 2) AbortSignal.any() - creates a signal that combines multiple signals 3) AbortSignal.abort() - creates an already aborted signal”

“The AbortSignal.timeout() method accepts a single argument, which is the number of milliseconds to wait, and returns an instance of AbortSignal that automatically aborts after the milliseconds elapse. You can then pass this instance of AbortSignal to an abortable function without creating an AbortController.”

“The AbortSignal.any() method is similar to Promise.any() in that it is used to combine multiple signals into one, allowing any of the signals to trigger an abort.”

“The AbortSignal.abort() method creates a signal that is already in the aborted state. Similar to the abort() method of an AbortController, there is an optional argument to AbortSignal.abort(), which is the abort reason.”

"The ability to pass an already-aborted signal to an abortable function is helpful when you want to test the behavior of your code when a function is aborted. "

“The AbortSignal class provides more than just static methods for creating new instances; its instances also have two properties that you can use to create your own abortable functions: 1) aborted - a read-only boolean value indicating if the request was aborted 2) reason - a read-only value indicating the reason the request was aborted (either the default AbortError or the argument passed to abort())”

“Each AbortSignal object fires an abort event when the request is aborted, which is helpful if an abortable function performs non-abortable asynchronous operations in its body”

“All JavaScript runtimes track unhandled promise rejections in some way. Web browsers and Deno implement the algorithm specified in the HTML specification while Node.js implements its own solution. Both solutions rely on two events: one that is emitted when an unhandled promise rejection occurs and one that is emitted if a previously unhandled promise rejection has a rejection handler added. The unhandledrejection event is emitted on the globalThis object in web browsers and Deno whenever an unhandled rejection is detected. Event handlers for unhandledrejection receive an event object containing the type of event, the promise that was rejected, and the reason for the promise rejection. The rejectionhandled event is emitted when a previously unhandled promise rejection has a rejection handler added. Event handlers for rejectionhandled also receive an event object that also contains the event type, the promise, and the rejection reason. Node.js also uses two events, but they occur on the process object and have slightly different names: unhandledRejection and rejectionHandled. Event handlers for unhandledRejection receive the rejection reason and the promise as arguments; event handlers for rejectionHandled receive just the promise. Both approaches allow you to implement unhandled rejection reporting for your application by listening for both events and tracking the promises they provide in a separate location. You can then periodically check the list of promises to report them into your logging or reporting system.”

Read 3x Faster (promo) 

No comments:

Post a Comment

"Understanding JavaScript Promises" Nicholas C. Zakas

  Memory training (promo) Best book notes: “Each promise goes through a short lifecycle starting in the  pending  state, which indicates th...