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.
Configure tasks
Set duration and whether each task fails
Pick a pattern
Sequential, Promise.all, allSettled, for...of
Watch it run
Animated bars show real elapsed time
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
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
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
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
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
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
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.
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 |██████████████ | 800msThe 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 800msThe 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"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 1500msAbove 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.2xWhen 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.idPromise.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.
| Scenario | Pattern to select |
|---|---|
| A page that awaits three independent endpoints | Compare sequential with Promise.all |
| Proving a refactor to a reviewer or a teammate | Promise.all, Compare on |
| Deciding whether one flaky call should sink the page | Promise.all, then Promise.allSettled |
| Explaining why a dependent chain cannot be parallelised | Sequential await |
| Sizing a batch job over a list of records | for...of + await |
| Teaching the event loop in an onboarding session | Any, with Compare on |
Frequently Asked Questions
How does the simulation work?
- Each task: a JavaScript Promise wrapping a
setTimeoutof 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.all | Promise.allSettled | |
|---|---|---|
| On rejection | Entire call rejects immediately | Rejection collected alongside successes |
| Other tasks | Still complete, but outcome = failed | All settle normally |
| Use when | You need every result | Partial 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.
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.
// 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.
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, 4How 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.
// 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))));
}