better-all: Promise.all that schedules dependent tasks without you declaring the graph
Better Promise.all with automatic dependency optimization
At a glance
- What is it?
- better-all is an MIT-licensed TypeScript library that takes an object of async task functions, works out the dependencies from what each one awaits, and starts every task as early as it can. The judgement is that the ergonomics win is real for wide fan-out with dependencies, and that the dependency inference comes from how you write `await this.$.x`, which is also its sharpest edge.
- Who is it for?
- better-all fits a TypeScript codebase that fans out into many independent calls, some of which depend on others, and that has outgrown hand-written `Promise.all` chains where the parallelism was decided by hand and silently became suboptimal when latencies shifted.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 80 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem is that optimal parallelism is not static
better-all is described as Promise.all with automatic dependency optimization and full type inference, and the README spends most of its length on why the manual approach breaks down.
The common pattern looks like this:
// Common pattern: Sequential execution wastes time
const [a, b] = await Promise.all([getA(), getB()]) // a: 1s, b: 10s → takes 10s
const c = await getC(a) // c: 10s → takes 10s
// Total: 20 secondsYou can fix it by hand, running `b` and `c` in parallel. But the fix only holds for the durations you assumed. If `getA()` now takes 10 seconds and `getC()` takes 1 second, the manual version takes 20 seconds where the naive one takes 11, because `c` waited on `a` before it even started.
So the manual optimisation is not just verbose, it is fragile in a specific way: it encodes a schedule, and the schedule is only correct for one set of latencies. The library's argument is that you would have to manually analyse and declare the dependency graph to do this correctly, and that the code for it becomes unmanageable in real-world scenarios with many tasks and complex dependencies, not to mention the loss of readability.
The README also points at a discussion thread for more downsides of the naive approach in real application code, and at the same time makes the practical point that this is not only about speed. The feature list names the uncaught dangling promises problem often seen in manual optimisation as one of the things the library avoids, which is the more serious of the two failure modes.
The this.$ object is how a dependency is declared
The API is small and the mechanism is one object.
import { all } from 'better-all'
const { a, b, c } = await all({
async a() { return getA() }, // 1s
async b() { return getB() }, // 10s
async c() { return getC(await this.$.a) } // 10s (waits for a)
})
// Total: 11 seconds - optimal parallelization!`all` automatically kicks off all tasks immediately, and when hitting an `await this.$.dependency`, it waits for that specific task to complete. The `this.$` object gives access to all other task results as promises, which lets you express dependencies naturally, and the library ensures maximal parallelization automatically.
The mechanism is worth being precise about, because it is the part that determines what the library can and cannot see. A dependency is not declared in a separate structure. It is declared by awaiting a promise obtained from `this.$`. The scheduler sees the await, and starts the dependent task when the awaited one resolves.
That has an important consequence. The dependency is only visible if you write it as `await this.$.name`. If you capture the result some other way, for instance by assigning `this.$.a` to a variable without awaiting it inside the task, or by reading it after the fact, the scheduler has nothing to key on and will start the task immediately.
So the library replaces one kind of explicitness with another. Instead of declaring a graph in a second object, you declare edges by how you consume the values, which is more natural but also less visible at a glance, and a dependency written the wrong way is silently serialised rather than reported.
Three entry points with different failure semantics
There are three exported functions, and the difference between them is mostly about what happens when a task fails.
`all(tasks, options?)` executes tasks with automatic dependency resolution and returns a promise that resolves to an object with all task results. It rejects if any task fails, like `Promise.all`.
`allSettled(tasks, options?)` returns settled results for all tasks, resolving to an object with each result as `{ status: 'fulfilled', value }` or `{ status: 'rejected', reason }`. It never rejects, since failed tasks are included in the result, like `Promise.allSettled`. There is one subtlety worth noting: if a task depends on a failed task, the dependent task will also fail unless it catches the error. So the batch survives, but the dependent branch does not.
`flow<R>(tasks, options?)` adds early exit support. The type parameter is required and specifies the return type that `$end()` must accept. Each task receives `this.$`, `this.$signal` and `this.$end(value: R)`, a function to exit the entire flow early with a return value of type `R`. It returns a promise that resolves to `R | undefined`: the value passed to the first `$end()` call, or `undefined` if no task calls `$end()`.
Options are the same across the three: `debug` set to `true` outputs a waterfall chart showing the task execution timeline, and `signal` is an `AbortSignal` to abort all tasks externally.
The abort semantics differ between `all` and `allSettled`. In `all`, `this.$signal` is an `AbortSignal` that aborts when any sibling task fails, giving auto-abort on failure. In `allSettled`, the signal only aborts on the external signal, not on sibling failure, which is consistent with a variant whose purpose is to collect every outcome rather than to stop at the first error.
Type inference comes from the return type of each task
The TypeScript support is a direct consequence of the object-based API, and it is the part of the design that makes the library pleasant to use in a typed codebase.
const result = await all({
async num() { return 42 },
async str() { return 'hello' },
async combined() {
const n = await this.$.num // n: number (auto-inferred!)
const s = await this.$.str // s: string (auto-inferred!)
return `${s}: ${n}`
}
})
result.num // number
result.str // string
result.combined // stringBecause the tasks are object properties rather than array elements, each one keeps its own name in the type system, so the awaited dependency is typed by the return type of the task it refers to. There is no annotation to write and no generic to specify.
The examples in the README show the dependency graph building up. A simple case has three tasks with no dependencies, all of which run in parallel. The dependency case fetches a user and then two other resources that both need the user id, and the comment explains the ordering: the user runs first, then profile and settings run in parallel. A complex dependency case chains five tasks, with `c` depending on `a` and `e` depending on `c`.
The `this.$` naming convention is a little unusual, and the dollar sign in an identifier is legal in JavaScript and TypeScript. It reads as a namespace rather than a variable, which is presumably why it was chosen, and it avoids any chance of colliding with a task named `$`.
The type inference is described as full, covering both results and dependencies. The catch is the same one as the runtime behaviour: the inference works off the awaited expressions inside each task, so a dependency that is not awaited in that exact form will not be typed as a dependency either.
Debug mode and the size of the dependency graph
The feature list has two entries that are about observability rather than speed.
The first is debug mode with waterfall visualization, described as letting you see exactly how tasks execute with ASCII waterfall charts. That is set with the `debug` option, and the API section repeats it for all three functions. For a library whose entire purpose is scheduling, a view of the schedule is the sensible debugging tool, and an ASCII chart fits the terminal rather than a browser.
The second is early exit support, which is what `flow` exists for. Exit flows early when a result is determined, which is a different shape of problem from maximal parallelism: sometimes you do not want every task, you want the first answer that settles the question. The `flow<R>` function gives each task a `this.$end(value: R)` call, and the first one to call it decides the result for everyone.
The stated features also list full type inference, automatic maximal parallelization, an object-based API with minimal cognitive load, no hanging promises, auto-abort on failure, early exit, and being lightweight with minimal dependencies and a small bundle size.
The scale question is the honest limit. The library's value grows with the number of tasks and the depth of the dependency graph, because that is where hand-written scheduling stops being readable. With three independent calls, `Promise.all` is one line and better. With a hundred calls across six levels of dependency, hand-maintaining the correct schedule is a real maintenance burden and the library earns its place.
The package metadata is consistent with the lightweight claim. The `dependencies` field is empty, the build is `bunchee`, the test runner is Vitest with a coverage variant, and the published `files` array contains only `dist`. The `exports` map offers a modern `import` condition pointing at the ES module build and a `require` condition pointing at a CommonJS build, with types for both.
Where this stops being the right tool
There are three honest limits, and they are worth stating before anyone rewrites a call site.
The first is that the dependency graph is invisible. With `Promise.all` you can see at a glance which calls are grouped and which are sequential. With `better-all`, the graph is spread across the bodies of the task functions, and a reader has to look for `await this.$.x` to reconstruct it. For a codebase other people maintain, that is a real cost, and the debug waterfall is a mitigation rather than a fix.
The second is the inference requirement. The scheduler works from awaited `this.$` expressions. A dependency expressed any other way is not a dependency to the scheduler, and it will be started immediately, which means a silent serialisation rather than a reported error. The same applies to types. A refactor that changes `await this.$.a` to `this.$.a` inside a promise chain would change behaviour and the compiler would not object.
The third is the version. The package is at 0.0.9, and the release history is 0.0.7 on 2026-02-01, 0.0.8 on 2026-07-21 and 0.0.9 on 2026-08-19, with the last push on 2026-08-19. At a 0.0.x version there is no stability commitment at all, and the API surface of a project in that state can change between minor versions. For a library whose whole appeal is that it is a drop-in replacement for `Promise.all`, that is the risk to weigh.
On the other side, the `allSettled` and `flow` variants do not exist as drop-ins for anything in the standard library, and they are the parts of this package that a team would be adopting on their own merits rather than as a replacement.
Editorial conclusion
better-all fits a TypeScript codebase that fans out into many independent calls, some of which depend on others, and that has outgrown hand-written `Promise.all` chains where the parallelism was decided by hand and silently became suboptimal when latencies shifted. It does not fit a codebase with two or three calls, where plain `Promise.all` is clearer, and it does not fit code that depends on the exact ordering semantics of `Promise.all` in a way the library does not preserve. Verify first that the dependency you mean is expressed as an `await` of `this.$.name` inside the task, since that is the only form the scheduler can see, that the `allSettled` variant is the one you want if a failed dependency should not abort the whole batch, and that the early exit path via `flow<R>` is typed correctly, since the return type parameter is required there.
Frequently asked questions
How do I install better-all?
The README lists four package managers: `npm install better-all`, `pnpm add better-all`, `bun add better-all` and `yarn add better-all`. The package is published under the name `better-all` and is MIT licensed.
How do I declare a dependency between two tasks in better-all?
You await the other task's promise from `this.$` inside the task that needs it, for example `async c() { return getC(await this.$.a) }`. The scheduler sees that await and starts the dependent task only once the awaited one completes, while every task without such an await starts immediately.
What is the difference between all and allSettled in better-all?
`all` rejects if any task fails, like `Promise.all`, and the `this.$signal` given to each task aborts when a sibling fails. `allSettled` never rejects and returns each result as `{ status: 'fulfilled', value }` or `{ status: 'rejected', reason }`, and its signal only aborts on an external signal. A task that depends on a failed task still fails in both unless it catches the error.
What does the flow function in better-all do?
It adds early exit to the dependency scheduling. Each task receives a `this.$end(value: R)` function, and the first call decides the result for the whole flow. The type parameter is required and specifies the return type that `$end()` must accept, and the returned promise resolves to `R | undefined`, with `undefined` when no task calls `$end()`.
How do I see the order better-all executed my tasks in?
Set the `debug` option to `true` and the library outputs a waterfall chart showing the task execution timeline. The option is available on `all`, `allSettled` and `flow` alike.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/shuding-better-all)Was this useful?