xSKILLby Infirals
Register

JavaScript for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

Promises and chaining

Async JavaScript16 min readLesson 2 of 4

2.1 A promise is a future value

A promise represents a value that will exist later. It starts pending, then becomes fulfilled with a value or rejected with a reason. Once settled, it never changes state. That one-way transition makes a promise safe to pass around: any number of handlers can attach, and all of them see the same outcome.

Promises replace callbacks as the universal contract for asynchronous work. fetch returns one, timers can be wrapped in one, and an async function always returns one. The API is uniform, and the caller decides how to wait.

Creating a promise directly uses the Promise constructor with an executor function that receives resolve and reject. The executor runs immediately. Resolving moves the promise to fulfilled; rejecting moves it to rejected. A thrown error inside the executor rejects too.

2.2 Building one

The example below wraps a timer so the delay becomes an awaitable value. The executor resolves after the timeout, and calling the function returns a pending promise that settles on the next few ticks.

A promise is not lazy. The executor runs when the promise is created, so work starts immediately even before a handler is attached. If work must wait for a handler, wrap it in a function that creates the promise when called.

javascript
function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

wait(10).then(() => {
  console.log("resolved after wait");
});

Output

resolved after wait

2.3 then, catch and finally

then registers a handler for the fulfilled value and returns a new promise. The return value of the handler becomes the value of that new promise. catch is shorthand for a then with no success handler and an error handler. finally runs for both outcomes and passes the original result through.

Each call returns a new promise, which is what makes chaining work. A chain is a description of a sequence: each step receives the previous step's value, transforms it, and passes the result on. An exception inside any step skips the remaining success handlers and jumps to the nearest catch.

javascript
Promise.resolve(5)
  .then((value) => value * 2)
  .then((value) => {
    if (value > 5) throw new Error("too big");
    return value;
  })
  .catch((error) => console.log("caught: " + error.message))
  .finally(() => console.log("done"));

Output

caught: too big
done

A catch placed in the middle of a chain handles errors from the steps above it and then allows the chain to continue with the value the handler returns. That placement is useful for optional work, such as a missing cache entry that falls back to a network call.

An unhandled rejection is an error that reached the end of a chain with no catch. Browsers report it in the console, and Node treats it as fatal by default in recent versions. Attach a catch at the end of every chain that can fail, or use an async function with try and catch.

finally is for cleanup: closing a connection, hiding a spinner, restoring a disabled button. It does not receive the value and it does not change the outcome unless it throws.

2.4 Running promises together

Promise.all takes an iterable of promises and returns one promise for an array of values. It fulfils when every input fulfils and rejects as soon as one rejects. Use it when all results are required and failure of one should fail the whole step.

Promise.allSettled waits for every promise and returns objects with status fulfilled or rejected. It never rejects for an input rejection, which suits dashboards where one failed panel should not blank the page. Promise.race settles with the first promise to settle, useful for a timeout.

These combinators do not start work; the promises were already running. What they change is how the results are awaited. That distinction explains why Promise.all is not a batch scheduler for lazy work.

javascript
const slow = new Promise((resolve) => setTimeout(() => resolve("slow"), 20));
const fast = Promise.resolve("fast");

Promise.all([slow, fast]).then((values) => {
  console.log(values.join(","));
});

Promise.allSettled([
  Promise.resolve("ok"),
  Promise.reject(new Error("bad")),
]).then((results) => {
  console.log(results.map((result) => result.status).join(","));
});

Output

fulfilled,rejected
slow,fast

2.5 Turning a callback into a promise

Older libraries take a callback for asynchronous work. Wrapping that call in a promise makes it usable with then and await. The wrapper returns a new promise and calls resolve or reject from the callback.

The example below wraps a timer. The same shape converts a callback-based file read, a database driver call or a legacy HTTP helper. A rejection is sent with the error object, and the caller catches it like any other promise failure.

A wrapper should handle a callback that might be called twice. Promises settle once, so the second call is ignored, which is usually the desired behaviour. A wrapper that calls resolve after reject has no effect, and a single catch in the chain sees only the first outcome.

javascript
function delay(ms, value) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}

delay(10, "finished").then((message) => console.log(message));

Output

finished

2.6 Shortcuts for settled promises

Promise.resolve(value) creates a promise that is already fulfilled, and Promise.reject(reason) creates one that is already rejected. Both are useful in tests and in functions that sometimes return a plain value and sometimes a promise.

Returning Promise.resolve(value) from a function with a promise contract keeps callers consistent. It also starts no work, so it is cheap. Promise.reject is less common in application code because throwing inside an async function or a then handler produces the same rejection.

A function that returns a value directly is often awaitable enough, because await accepts a plain value and continues immediately. The shortcut matters when a contract explicitly promises a promise object for chaining.

2.7 When not to use promises

Promises are for one future value. A stream of values, a web socket or a sequence of clicks is a different shape; events and async iterators fit better. Wrapping an event stream in a single promise loses every value after the first.

A promise also cannot be cancelled directly. Cancelling a fetch uses an AbortController, and cancelling other work needs a flag that the work checks. A promise that is abandoned still runs its executor, which is why a request that is no longer needed should be aborted rather than ignored.

2.8 Common mistakes

  • Forgetting that then returns a new promise and writing handlers on the wrong object.
  • Catching an error in the middle of a chain and expecting later steps to skip.
  • Using Promise.all when one failure should not blank a dashboard.
  • Creating a promise inside a loop and awaiting it sequentially instead of together.
  • Not returning a promise inside a then handler, which breaks the chain timing.
  • Leaving a rejected promise unhandled.

Exam

Promise questions trace a chain. Start with the resolved value, apply each then in order, stop at the first throw or rejection, and follow the nearest catch. For combinators, decide whether one rejection fails the whole result.

  • Wrap a timer in a promise and log the resolved value.
  • Build a three-step then chain that transforms a number.
  • Throw inside a then and confirm where the catch runs.
  • Run two promises with all and allSettled and compare the results.