Dev Encyclopedia
ArticlesToolsContactAbout

Get notified when new content drops

No spam. Just new articles, tools, and updates straight to your inbox.

Dev Encyclopedia

A reference for builders

Dev.to
Discord
WhatsApp Channel
daily.dev
Hashnode
X

Content

  • Articles
  • Tools
  • About
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

Legal

  • Privacy Policy
  • Terms of Service
  • Disclaimer

© 2026 Dev Encyclopedia

Back to top ↑
  1. Home
  2. /
  3. Tools
  4. /
  5. Async Timing Visualizer
Free · Live · No API key

See why Promise.all is faster. Watch it happen.

Configure async tasks, pick a pattern (sequential, Promise.all, or Promise.allSettled), and watch an animated Gantt chart show exactly when each task runs. Add failures to see how each pattern responds.

  1. 1

    Configure tasks

    Set duration and whether each task fails

  2. 2

    Pick a pattern

    Sequential, Promise.all, allSettled, for...of

  3. 3

    Watch it run

    Animated bars show real elapsed time

Zeeshan Tofiq

Zeeshan Tofiq

Full Stack Developer

How the Async Timing Visualizer works

Every bar you see comes from a real Promise settling in your browser. Nothing is pre-rendered, and no timings are hard-coded into the chart.

  1. 1

    You describe the async work

    Add up to six tasks. Each one has a duration slider (200ms to 3000ms) and a fail toggle. A task stands in for one unit of I/O: an HTTP request, a database query, a file read, a queue publish.

  2. 2

    You pick an execution pattern

    Sequential await, Promise.all, Promise.allSettled, or for...of with await. The description under each option tells you how that pattern behaves when one of the tasks rejects.

  3. 3

    Each task becomes a real Promise

    On Run, every task is wrapped in a Promise around setTimeout(duration). Tasks with the fail toggle on call reject(); the rest call resolve(). The chosen pattern then drives them exactly as your own code would.

  4. 4

    Bars are drawn from wall-clock timestamps

    Date.now() is recorded when a promise is created and again when it settles. The bar starts at the first offset and stops growing at the second, so the chart is a recording of what happened, not a prediction.

  5. 5

    The summary reports total elapsed time

    When the run finishes, a banner shows the total wall-clock duration and the outcome: completed, completed with failures, or failed. With Compare with sequential enabled, it also prints the speedup factor between the two runs.

  6. 6

    The equivalent JavaScript is generated for you

    The panel at the bottom of the tool prints working code for your exact task list and pattern, with the expected total in a comment. Change a duration or a pattern and the snippet updates with it.

What the timeline output means

The chart encodes four things at once: when a task started, how long it ran, whether it settled successfully, and how the whole run compares to doing the same work one call at a time.

A growing coloured bar: In flight

The task's promise has been created and its timer is still counting down. The bar grows left to right every 80ms while the run is live. Its left edge is the moment the promise was created, measured from the start of the run.

Task A  |████████▌            |  1000ms
Task B  |████▌                |  1500ms   ← still running
Task C  |██████████████       |   800ms
A finished bar with a millisecond label: Fulfilled

The promise resolved. The number printed inside the bar is the offset from the start of the run at which it settled, so a bar labelled 2300ms in a sequential run tells you the task did not even begin until earlier work was done.

Sequential:  Task A ends 1000ms, Task B ends 2500ms, Task C ends 3300ms
Promise.all: Task A ends 1000ms, Task B ends 1500ms, Task C ends  800ms
A red bar and a cross next to the task name: Rejected

The fail toggle was on, so the task called reject() when its timer fired. Under Promise.all the whole call rejects at that instant, while the other timers still finish in the background. Under Promise.allSettled the rejection is just one entry in the results array.

Task B ✕   rejected at 1500ms
Promise.all      -> run marked failed
Promise.allSettled -> run marked "completed with failures"
Staircase shape vs block shape: The whole point

Sequential and for...of produce a staircase: each bar starts where the previous one ended. Promise.all and Promise.allSettled produce a block: every bar starts at zero and the run ends with the slowest task. Same work, different wall-clock cost.

Staircase (sequential)      Block (Promise.all)
|███                    |   |███                    |
|   ████                |   |█████                  |
|       ██              |   |██                     |
total 3300ms                total 1500ms
The timing preview box: Predicted, not measured

Above the Run button, Sequential total is the sum of every duration and Parallel total is the longest single duration. The speedup is the ratio between them. It is arithmetic on your slider values, which is why the measured run lands a few milliseconds higher: timers and the event loop add real overhead.

Tasks: 1000ms + 1500ms + 800ms
Sequential total  3300ms
Parallel total    1500ms   (the slowest task)
Speedup           2.2x

When to use each pattern

Why it matters

3 independent 1-second API calls: sequential = 3s total, Promise.all = 1s total. The difference grows with every additional request.

Sequential await

Task B depends on Task A's result: fetch a user, then fetch their orders using the user ID.

const user = await fetchUser();
const orders = await fetchOrders(user.id);

Promise.all

Independent tasks where you need all results. One failure aborts everything.

const [user, posts] = await Promise.all([
  fetchUser(), fetchPosts()
]);

Promise.allSettled

Independent tasks where partial results are acceptable (a dashboard that shows what it can).

const results = await Promise.allSettled([
  fetchUser(), fetchPosts()
]);

for...of + await

Sequential processing of an array: each item must complete before the next begins.

for (const id of orderIds) {
  await processOrder(id);
}

Async syntax reference

The exact syntax behind each timeline shape, plus the edge cases the visualizer cannot draw for you: result shapes, error handling, and the loop that silently does nothing.

Sequential await (dependent work)

Use it only when the second call needs the first call's result. Every extra await adds its full duration to the total.

// Total = 1000ms + 1500ms
const user = await fetchUser(id);          // ~1000ms
const orders = await fetchOrders(user.id); // ~1500ms, needs user.id

Promise.all (independent work, all results required)

Start every call before awaiting. Promise.all rejects as soon as any input rejects, and the rejection reason is the first error, not an array of errors.

// Total = the slowest call, not the sum
const [user, posts, settings] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchSettings(id),
]);

// One failure aborts the destructuring, so handle it
try {
  const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);
} catch (err) {
  // err is the first rejection reason
}

Promise.allSettled (partial results are acceptable)

Never rejects. Every entry is an object with a status field, so you must narrow before reading value.

const results = await Promise.allSettled([fetchUser(id), fetchPosts(id)]);

// results[i] is either
//   { status: "fulfilled", value: <resolved value> }
//   { status: "rejected",  reason: <error> }

const ok = results
  .filter((r) => r.status === "fulfilled")
  .map((r) => r.value);

const failures = results
  .filter((r) => r.status === "rejected")
  .map((r) => r.reason);

for...of + await (ordered processing)

Identical timing to stacked awaits. Correct when each iteration must finish before the next one starts, for example writing rows in order or respecting a rate limit.

for (const id of orderIds) {
  await processOrder(id); // one at a time, in order
}

The forEach trap (looks async, awaits nothing)

Array.prototype.forEach ignores the promise its callback returns, so the surrounding function continues immediately and errors become unhandled rejections. This is the single most common async bug in review.

// Broken: finishes instantly, results are never awaited
orderIds.forEach(async (id) => {
  await processOrder(id);
});

// Fix A: sequential and ordered
for (const id of orderIds) {
  await processOrder(id);
}

// Fix B: concurrent, collect every result
await Promise.all(orderIds.map((id) => processOrder(id)));

Promise.race and Promise.any (first one wins)

race settles with the first promise to settle, fulfilled or rejected, which makes it the standard timeout pattern. any ignores rejections and waits for the first fulfilment, rejecting with an AggregateError only if all inputs reject.

// Timeout: whichever settles first wins
const data = await Promise.race([
  fetchData(),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("timeout")), 3000)
  ),
]);

// First successful mirror, failures ignored
const fastest = await Promise.any([
  fetch("https://mirror-1.example.com/file"),
  fetch("https://mirror-2.example.com/file"),
]);

Limiting concurrency (200 items, 10 at a time)

Promise.all with a huge array opens every connection at once and will get you rate limited or exhaust the socket pool. Chunk the input and await each chunk.

async function inChunks(items, size, worker) {
  const out = [];
  for (let i = 0; i < items.length; i += size) {
    const chunk = items.slice(i, i + size);
    out.push(...(await Promise.all(chunk.map(worker))));
  }
  return out;
}

const users = await inChunks(userIds, 10, fetchUser);

When to use the visualizer

Model the code you actually have: one task per real call, durations set to what you see in the network panel or your traces.

ScenarioTask setupPattern to select
A page that awaits three independent endpoints3 tasks at your real p95 latenciesCompare sequential with Promise.all
Proving a refactor to a reviewer or a teammateDurations copied from the network tabPromise.all, Compare on
Deciding whether one flaky call should sink the pageSame tasks, fail toggle on the flaky onePromise.all, then Promise.allSettled
Explaining why a dependent chain cannot be parallelised2 tasks, the second longer than the firstSequential await
Sizing a batch job over a list of recordsUp to 6 tasks of equal durationfor...of + await
Teaching the event loop in an onboarding sessionDurations far apart, for example 300ms and 3000msAny, with Compare on

Frequently Asked Questions

How does the simulation work?
  • Each task: a JavaScript Promise wrapping a setTimeout of the configured duration.
  • Sequential mode: awaits each task before starting the next.
  • `Promise.all` mode: starts all tasks simultaneously, resolves when every task finishes.
  • `Promise.allSettled` mode: same as above, but collects all results even when some tasks fail.
  • Animation: bars track real wall-clock time using Date.now(). Nothing is faked or pre-calculated.
What does the fail toggle on each task do?

When toggled, the task's Promise rejects instead of resolves when its timer fires.

Promise.allPromise.allSettled
On rejectionEntire call rejects immediatelyRejection collected alongside successes
Other tasksStill complete, but outcome = failedAll settle normally
Use whenYou need every resultPartial success is acceptable
What does 'compare with sequential' do?
  • Sequential timeline: bars stack one after another, each task waiting for the previous.
  • Parallel timeline: bars all start at the same time, showing true concurrency.
  • Both run simultaneously so you see the timing difference side by side in real time.
Does this reflect real-world async performance?

Yes, for I/O-bound work like API calls and database queries. The timing ratios match what you would see with real requests of the same durations.

ℹ Info

Promise.all does not help with CPU-bound computation. JavaScript is single-threaded. Most async slowdowns in web apps are I/O, not CPU, so Promise.all gives real speed gains in practice.

Why does for...of look the same as sequential?

Because it is. for...of with await is sequential execution in a loop: each iteration awaits the previous before starting the next.

javascript
// These two are identical in timing behavior:

// Option 1: explicit sequential awaits
const a = await fetchA();
const b = await fetchB();

// Option 2: for...of with await (same timing, cleaner for arrays)
for (const id of ids) {
  await processItem(id);
}
What is the difference between a microtask and a macrotask?

Both are queues the event loop drains, but they have different priority. Promise callbacks (.then, .catch, .finally, and the code after an await) go on the microtask queue. Timers (setTimeout, setInterval) and I/O callbacks go on the macrotask queue.

After the current synchronous code finishes, the engine empties the entire microtask queue before it picks up a single macrotask. That is why a resolved promise always logs before a zero-delay timer.

javascript
console.log("1: synchronous");

setTimeout(() => console.log("4: macrotask (timer)"), 0);

Promise.resolve().then(() => console.log("3: microtask"));

console.log("2: synchronous");

// Output order: 1, 2, 3, 4

ℹ Info

This matters for the visualizer: each task's timer is a macrotask, but the pattern logic that reacts to it (Promise.all settling, the next loop iteration resuming) runs as a microtask. That is where the few milliseconds of overhead between the predicted total and the measured total come from.

How many promises can I safely pass to Promise.all?

There is no language limit: Promise.all accepts any iterable. The limit is on the other side of the call. Passing 500 fetches starts 500 requests at once, which triggers API rate limits, exhausts database connection pools, and can push memory hard if each response is large.

The visualizer caps at six tasks for readability, but the shape of the chart is the point: beyond a handful of calls you want bounded concurrency rather than everything at once.

javascript
// Risky at scale: 500 concurrent requests
const all = await Promise.all(ids.map(fetchUser));

// Bounded: 10 at a time
const out = [];
for (let i = 0; i < ids.length; i += 10) {
  const chunk = ids.slice(i, i + 10);
  out.push(...(await Promise.all(chunk.map(fetchUser))));
}

💡 Tip

A rough starting point: 5 to 10 concurrent calls per external API, and never more concurrent database queries than your connection pool size.

Related reading

Guide

5 async/await Mistakes That Slow Your JavaScript Code

The companion article: sequential awaits, await in forEach, missing Promise.all, and more with before/after fixes.

Guide

npm Scripts You're Probably Not Using

run-p and run-s let you run npm scripts in parallel or series, the same tradeoff you just visualized, applied to your build pipeline.

Zeeshan Tofiq

Zeeshan Tofiq

Full Stack Developer

Full stack developer with over 6 years of experience building production applications. Writes practical guides on JavaScript, TypeScript, React, Node.js, and cloud infrastructure. Focused on helping developers solve real problems with clean, maintainable code.

Enjoyed this article?

Get practical dev guides, tool updates, and new articles delivered straight to your inbox. No spam, unsubscribe anytime.