PureDevTools

JavaScript Promise Visualizer

See Promise chains as visual timelines — pending, fulfilled, rejected states with timing and value annotations

All processing happens in your browser. No data is sent to any server.

Select an example

A single promise that resolves successfully.

State:PENDINGFULFILLEDREJECTEDType:new Promise.then().catch().finally()combinator
Total duration: 500ms2 fulfilled
0ms500ms
1.new PromiseFULFILLED0ms400ms

new Promise((resolve) => ...)

400ms
value:"Hello, World!"
2..then()FULFILLED400ms500ms

.then(value => console.log(value))

100ms
value:undefined

How to read this visualization

  • Green / Fulfilled — promise resolved with a value; the next .then() will run
  • Red / Rejected — promise failed with a reason;.then() is skipped, the next .catch() runs
  • Amber / Pending — promise has not yet settled
  • The timeline bar shows relative start and duration. Bars for concurrent promises (Promise.all / race) overlap.

You chain .then().then().catch().finally() and one of the intermediate .then() throws. Does .catch() get it? Does .finally() run? What about Promise.race() with a timeout — if the timeout fires first, does the other promise’s .then() still execute? Promise chains are hard to reason about because the execution order depends on which promises resolve first, and the microtask queue is invisible.

Why This Visualizer (Not the Promise Methods Reference)

PureDevTools has a Promise Methods Reference with all static and instance methods documented. This tool visualizes Promise chains as interactive timelines — you see pending, fulfilled, and rejected states with timing annotations, watch microtasks execute in order, and understand exactly when .catch() and .finally() fire. Use the reference for API details; use this visualizer to understand execution flow.

Understanding JavaScript Promises Visually

JavaScript Promises represent the eventual result of an asynchronous operation. While the mental model is straightforward — a Promise is either pending, fulfilled, or rejected — visualizing how states transition through a chain of .then(), .catch(), and .finally() calls makes the behavior far easier to reason about.

The Three Promise States

Every Promise exists in exactly one of three states at any point in time:

StateMeaningCSS Color Convention
PendingThe async operation is still in progressYellow / Amber
FulfilledThe operation completed successfully with a valueGreen
RejectedThe operation failed with a reason (error)Red

Once a promise transitions from pending to either fulfilled or rejected, it is settled and its state cannot change again. This immutability is a core property of the Promise specification.

const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve("done"), 300);
});

console.log(p); // Promise { <pending> }

p.then(val => {
  console.log(val); // "done"
  console.log(p);   // Promise { "done" }
});

Promise Chain Mechanics

.then(onFulfilled, onRejected)

.then() registers callbacks for fulfilled and rejected states. It always returns a new Promise — this is what enables chaining:

fetch('/api/data')
  .then(res => res.json())       // returns a new Promise
  .then(data => process(data))   // returns a new Promise
  .then(result => display(result)); // returns a new Promise

Key rules:

.catch(onRejected)

.catch(fn) is shorthand for .then(undefined, fn). It intercepts rejections anywhere in the preceding chain:

fetch('/api/data')
  .then(res => res.json())     // skipped if fetch rejects
  .then(data => render(data))  // skipped if either above rejects
  .catch(err => showError(err)); // catches any rejection above

After .catch() handles a rejection, the chain continues as fulfilled (unless .catch() itself throws or returns a rejected Promise).

.finally(onFinally)

.finally(fn) runs regardless of whether the promise fulfilled or rejected. It passes the original result through — it does not change the settled value unless it throws:

showSpinner();
fetch('/api/data')
  .then(data => display(data))
  .catch(err => handleError(err))
  .finally(() => hideSpinner()); // always runs

Parallel Promise Combinators

Promise.all()

Resolves when all input promises fulfill. Rejects immediately if any input rejects (fail-fast):

const [users, posts, comments] = await Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json()),
]);
// Total time = slowest of the three, not sum of all three

Use when: All operations must succeed and you need all results.

Promise.race()

Resolves or rejects with the first settled promise, ignoring the rest:

const result = await Promise.race([
  fetchFromCache(),   // resolves in 80ms
  fetchFromNetwork(), // resolves in 600ms
]);
// result = cache result (80ms), not network (600ms)

Use when: You want the fastest result, or to implement timeouts.

Promise.allSettled()

Resolves after all input promises settle (no fail-fast). Returns an array of outcome objects:

const results = await Promise.allSettled([p1, p2, p3]);
// [
//   { status: 'fulfilled', value: 'A' },
//   { status: 'rejected', reason: Error('B') },
//   { status: 'fulfilled', value: 'C' },
// ]

Use when: You want results from all promises regardless of individual failures.

Promise.any()

Resolves with the first fulfilled promise (ignoring rejections). Rejects only when all input promises reject:

const fastest = await Promise.any([
  fetchFromServer1(),
  fetchFromServer2(),
  fetchFromServer3(),
]);
// First server to succeed wins

Use when: You only need one success from multiple attempts.

Common Patterns

Sequential Async Operations

async function sequential() {
  const user = await fetchUser(id);        // wait for user
  const profile = await fetchProfile(user.profileId); // then fetch profile
  const posts = await fetchPosts(user.id); // then fetch posts
  return { user, profile, posts };
  // Total time = fetchUser + fetchProfile + fetchPosts
}

Parallel Async Operations

async function parallel() {
  const [user, settings] = await Promise.all([
    fetchUser(id),
    fetchSettings(id),
  ]);
  return { user, settings };
  // Total time = max(fetchUser, fetchSettings) — much faster
}

Error Recovery with Default Values

const data = await fetch('/api/data')
  .then(res => res.json())
  .catch(() => ({ items: [], error: true })); // default on failure

Timeout Pattern with Promise.race()

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

await withTimeout(fetch('/api/slow'), 3000);

Retry Pattern

async function retry(fn, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxAttempts) throw err;
      await new Promise(r => setTimeout(r, 200 * attempt)); // exponential backoff
    }
  }
}

async/await vs Promise Chains

async/await is syntactic sugar over Promises — both produce identical behavior:

// Promise chain
fetch('/api')
  .then(res => res.json())
  .then(data => process(data))
  .catch(err => handle(err));

// Equivalent async/await
async function load() {
  try {
    const res = await fetch('/api');
    const data = await res.json();
    return process(data);
  } catch (err) {
    handle(err);
  }
}

async/await is generally preferred for readability in sequential flows. Promise chains remain natural for parallel combinators (Promise.all, Promise.race) and when composing streams of operations.

Promise Anti-Patterns to Avoid

Callback Hell inside Promises (Nesting)

// Bad — nested promises lose chaining benefits
fetch('/api/a').then(a => {
  fetch('/api/b').then(b => { // nested!
    fetch('/api/c').then(c => { /* ... */ });
  });
});

// Good — flat chain
fetch('/api/a')
  .then(a => fetch('/api/b'))
  .then(b => fetch('/api/c'))
  .then(c => { /* ... */ });

Forgetting to Return

// Bad — breaks the chain (next .then gets undefined)
.then(data => {
  transform(data); // no return!
})

// Good
.then(data => transform(data))

Swallowed Errors

// Bad — silent failure
fetch('/api').then(process).catch(() => {}); // empty catch hides bugs

// Good
fetch('/api').then(process).catch(err => console.error(err));

Frequently Asked Questions

What is a JavaScript Promise? A Promise is an object representing the eventual completion or failure of an asynchronous operation. It provides a cleaner alternative to nested callbacks by chaining .then() and .catch() handlers in a flat, readable structure.

What is the difference between Promise.all and Promise.allSettled? Promise.all rejects immediately if any input promise rejects (fail-fast). Promise.allSettled waits for all promises to settle and always resolves with an array of outcome objects, letting you inspect which succeeded and which failed.

Can a Promise change state after it has resolved? No. Once a Promise transitions from pending to fulfilled or rejected, it is permanently settled. Calling resolve() or reject() a second time has no effect.

Does this tool execute my JavaScript code? No. All examples are pre-built scenarios. The visualizer renders pre-computed timeline data entirely in your browser — no code is evaluated or sent to any server.

How does async/await relate to Promises? async functions always return a Promise. await pauses the async function until a Promise settles and extracts its value (or throws on rejection). Every async/await pattern can be rewritten as equivalent Promise chains.

Related Tools

More JavaScript Tools