shuding/better-all
A look at shuding/better-all: 1.1k stars on GitHub, written primarily in TypeScript. Better Promise.all with automatic dependency optimization
Snapshot summary built from the project's own GitHub metadata — there's no written TopGit review yet. The page will update automatically when a full review is published.
TopGit writes full reviews for the most-starred, most-requested repositories. This page is a snapshot until then — see the READ ME tab for the original README in full.
Snapshot
Top contributors
Show top contributors
better-all
Promise.all with automatic dependency optimization and full type inference.
Why?
When you have tasks with dependencies, the common Promise.all pattern is sometimes inefficient:
// 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 seconds
You could optimize this manually by parallelizing b and c:
const a = await getA() // a: 1s -> takes 1s
const [b, c] = await Promise.all([ // b: 10s, c: 10s -> takes 10s
getB(),
getC(a)
])
// Total: 11 seconds
But what if the durations of these methods change (i.e. unstable network latency)? Say getA() now takes 10 seconds and getC() takes 1 second. The previous manual optimization becomes suboptimal again, compared to the naive approach:
const a = await getA() // a: 10s -> takes 10s
const [b, c] = await Promise.all([ // b: 10s, c: 1s -> takes 10s
getB(),
getC(a)
])
// Total: 20 seconds
// Naive approach:
const [a, b] = await Promise.all([getA(), getB()]) // a: 10s, b: 10s → takes 10s
const c = await getC(a) // c: 1s → takes 1s
// Total: 11 seconds
To correctly optimize such cases using Promise.all, you'd have to manually analyze and declare the dependency graph:
const [[a, c], b] = await Promise.all([
getA().then(a => getC(a).then(c => [a, c])),
getB()
])
This quickly becomes unmanageable in real-world scenarios with many tasks and complex dependencies, not to mention the loss of readability.
In real-world application code, there are more downsides of the naive approach and ad-hoc promise adjustments. Give this a read if you are still not convinced.
Better Promise.all
This library solves it automatically:
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 magical this.$ object gives you access to all other task results as promises, allowing you to express dependencies naturally.
The library ensures maximal parallelization automatically.
Installation
npm install better-all
# or
pnpm add better-all
# or
bun add better-all
# or
yarn add better-all
Features
- Full type inference: Both results and dependencies are fully typed
- Automatic maximal parallelization: Independent tasks run in parallel
- Object-based API: Minimal cognitive load, easy to read
- No hanging promises: Avoids the uncaught dangling promises problem often seen in manual optimization
- Auto-abort on failure: Cancel remaining tasks when one fails via
this.$signal - Debug mode with waterfall visualization: See exactly how tasks execute with ASCII waterfall charts
- Early exit support: Exit flows early when a result is determined
- Lightweight: Minimal dependencies and small bundle size
API
all(tasks, options?)
Execute tasks with automatic dependency resolution.
tasks: Object of async task functionsoptions: Optional configuration objectdebug: Set totrueto output a waterfall chart showing task execution timelinesignal: AnAbortSignalto abort all tasks externally
- Each task function receives:
this.$- an object with promises for all task resultsthis.$signal- anAbortSignalthat aborts when any sibling task fails
- Returns a promise that resolves to an object with all task results
- Rejects if any task fails (like
Promise.all)
allSettled(tasks, options?)
Execute tasks with automatic dependency resolution, returning settled results for all tasks.
tasks: Object of async task functionsoptions: Optional configuration objectdebug: Set totrueto output a waterfall chart showing task execution timelinesignal: AnAbortSignalto abort all tasks externally
- Each task function receives:
this.$- an object with promises for all task resultsthis.$signal- anAbortSignal(only aborts on external signal, not on sibling failure)
- Returns a promise that resolves to an object with all task results as
{ status: 'fulfilled', value }or{ status: 'rejected', reason } - Never rejects - failed tasks are included in the result (like
Promise.allSettled) - If a task depends on a failed task, the dependent task will also fail unless it catches the error
flow<R>(tasks, options?)
Execute tasks with automatic dependency resolution and early exit support.
- Type parameter
<R>: Required. Specifies the return type that$end()must accept tasks: Object of async task functionsoptions: Same asall()- optional configuration object- Each task function receives:
this.$- an object with promises for all task resultsthis.$signal- anAbortSignalfor resource cleanupthis.$end(value: R)- function to exit the entire flow early with a return value of typeR
- Returns a promise that resolves to
R | undefined- Returns the value passed to the first
$end()call - Returns
undefinedif no task calls$end()
- Returns the value passed to the first
- See Early Exit Flow for detailed usage
Examples
Basic Parallel Execution
const { a, b, c } = await all({
async a() { await sleep(1000); return 1 },
async b() { await sleep(1000); return 2 },
async c() { await sleep(1000); return 3 }
})
// All three run in parallel
// Returns { a: 1, b: 2, c: 3 }
With Dependencies
const { user, profile, settings } = await all({
async user() { return fetchUser(1) },
async profile() { return fetchProfile((await this.$.user).id) },
async settings() { return fetchSettings((await this.$.user).id) }
})
// User runs first, then profile and settings run in parallel
Type Safety
Full TypeScript support with automatic type inference:
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 // string
Complex Dependency Graph
const { a, b, c, d, e } = await all({
async a() { return 1 },
async b() { return 2 },
async c() { return (await this.$.a) + 10 },
async d() { return (await this.$.b) + 20 },
async e() { return (await this.$.c) + (await this.$.d) }
})
// a and b run in parallel
// c waits for a, d waits for b (c and d can overlap)
// e waits for both c and d
// { a: 1, b: 2, c: 11, d: 22, e: 33 }
console.log({ a, b, c, d, e })
Stepped Dependency Chain
In this example, the postsWithAuthor task calls await this.$.user and await this.$.posts sequentially but there won't be any actual delays. The all function will always kick off all tasks as early as possible, so posts was already running while we awaited this.$.user:
const result = await all({
async user() {
return fetchUser(1)
},
async posts() {
return fetchPosts((await this.$.user).id)
},
async postsWithAuthor() {
const user = await this.$.user
console.log(`Fetched user: ${user.name}`)
const posts = await this.$.posts
return posts.map(post => ({ ...post, author: user.name }))
},
})
This still gives optimal parallelization.
Debug Mode
Enable debug mode to visualize task execution with a waterfall chart:
const result = await all({
async config() {
await sleep(50)
return { apiUrl: 'https://api.example.com' }
},
async user() {
await sleep(120)
return { id: 1, name: 'Alice' }
},
async posts() {
const user = await this.$.user
await sleep(200)
return fetchPosts(user.id)
},
async profile() {
const user = await this.$.user
const config = await this.$.config
await sleep(80)
return fetchProfile(user.id, config.apiUrl)
},
async analytics() {
const posts = await this.$.posts
const profile = await this.$.profile
await sleep(40)
return computeAnalytics(posts, profile)
}
}, { debug: true })
This outputs an ASCII waterfall chart showing:
- Task execution timeline
- Task duration in milliseconds
- Dependencies for each task
- Visual representation of parallel vs sequential execution
Example output:
╔════════════════════════════════════════════════════════════════════════════════╗
║ Task Execution Waterfall ║
╠════════════════════════════════════════════════════════════════════════════════╣
║ Total Duration: 364.54ms ║
╚════════════════════════════════════════════════════════════════════════════════╝
Task │ Deps │ Duration │ Timeline
──────────┼────────────────┼──────────┼──────────────────────────────────────────────────────────────────
config │ - │ 51.4ms │ ████████
user │ - │ 121.4ms │ ████████████████████
posts │ user │ 322.6ms │ ░░░░░░░░░░░░░░░░░░░░██████████████████████████████████████
profile │ user, config │ 202.9ms │ ░░░░░░░░░░░░░░░░░░░░███████████████████
analytics │ posts, profile │ 364.4ms │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███████
Legend: █ = active (fulfilled), ▓ = active (rejected), ░ = waiting on dependency
The enhanced waterfall visualization shows:
- █ (solid bars) = Active execution time when the task is running its code
- ░ (light shade) = Waiting time when the task is blocked on a dependency
- ▓ (dashed bars) = Active execution for tasks that failed
This makes it easy to:
- Distinguish between active execution vs waiting on dependencies
- Identify which tasks are running in parallel
- See exactly how long each task actively executes vs waits
- Understand the dependency chain and blocking relationships
- Spot opportunities for optimization (e.g., tasks with long wait times)
Error Handling
With all()
Errors propagate to dependent tasks automatically, similar to Promise.all:
try {
await all({
async a() { throw new Error('Failed') },
async b() { return (await this.$.a) + 1 }
})
} catch (err) {
console.error(err) // Error: Failed
}
With allSettled()
All tasks complete and return their settled state, never rejecting:
const result = await allSettled({
async a() { return 1 },
async b() { throw new Error('Task b failed') },
async c() { return 3 }
})
// result.a: { status: 'fulfilled', value: 1 }
// result.b: { status: 'rejected', reason: Error('Task b failed') }
// result.c: { status: 'fulfilled', value: 3 }
if (result.a.status === 'fulfilled') {
console.log(result.a.value) // 1
}
if (result.b.status === 'rejected') {
console.error(result.b.reason) // Error: Task b failed
}
Handling Dependency Failures with allSettled()
When a task depends on a failed task, it will also fail unless the error is caught:
const result = await allSettled({
async a() { throw new Error('a failed') },
async b() {
// This will fail because 'a' failed
const aValue = await this.$.a
return aValue + 10
},
async c() {
// This handles the error and succeeds
try {
const aValue = await this.$.a
return aValue + 10
} catch (err) {
return 'fallback value'
}
}
})
// result.a: { status: 'rejected', reason: Error('a failed') }
// result.b: { status: 'rejected', reason: Error('a failed') }
// result.c: { status: 'fulfilled', value: 'fallback value' }
Abort Signal
When a task fails in all(), you may want to cancel other running tasks to avoid wasting resources (e.g., API calls, LLM requests).
Each task receives this.$signal - an AbortSignal that gets aborted when any sibling task fails:
const result = await all({
async fetchUser() {
const res = await fetch('/api/user', { signal: this.$signal })
return res.json()
},
async fetchPosts() {
// If fetchUser fails, this.$signal will be aborted
const res = await fetch('/api/posts', { signal: this.$signal })
return res.json()
}
})
You can also pass an external signal to respect parent abort controllers:
const controller = new AbortController()
const result = await all({
async a() { return fetchData(this.$signal) },
async b() { return fetchMoreData(this.$signal) }
}, { signal: controller.signal })
Note: allSettled() does NOT auto-abort on task failure (to preserve its "wait for all" behavior), but external signal abort still works.
Early Exit Flow
flow allows you to exit early from complex async flows when a task determines the final result. This is useful for optimization patterns like:
- Cache checks: Exit early if cached data is available
- Racing operations: Return the first successful result
- Conditional computations: Skip remaining work based on intermediate results
flow(tasks, options?)
Execute tasks with automatic dependency resolution, but allow any task to end the entire flow early by calling this.$end(value).
Key behaviors:
- All tasks start together in parallel (same as
all()) - First task to call
this.$end(value)determines the return value - After
$end()is called, other tasks that try to access dependencies will receive errors (caught silently) - Real errors (not from
$end) still propagate to the caller - Integrates with
this.$signalfor resource cleanup
Early Exit from Cache
import { flow } from 'better-all'
const data = await flow<YourDataType>({
async checkCache() {
const cached = await getFromCache('key')
if (cached) this.$end(cached) // Exit early with cached data
return null
},
async fetchFromApi() {
const user = await this.$.checkCache // Will throw if cache hit
return await fetchExpensiveData()
},
async processData() {
const apiData = await this.$.fetchFromApi
this.$end(transform(apiData))
}
})
Racing Operations
const result = await flow<ResponseData>({
async fetchFromPrimary() {
await sleep(100)
const data = await fetch('/api/primary')
this.$end(await data.json())
},
async fetchFromBackup() {
await sleep(500)
const data = await fetch('/api/backup')
this.$end(await data.json())
}
})
// Returns data from whichever endpoint responds first
Conditional Early Exit
const result = await flow<{ error: string } | ProcessedData>({
async validateInput() {
const isValid = await validate(input)
if (!isValid) this.$end({ error: 'Invalid input' })
return input
},
async processData() {
const validInput = await this.$.validateInput
const processed = await heavyComputation(validInput)
this.$end({ success: true, data: processed })
}
})
Return Type
You must specify the return type as a type parameter to flow:
const result = await flow<number | string>({
async task1() {
this.$end(42) // number
return 1
},
async task2() {
this.$end('hello') // string
return 'world'
}
})
// result: number | string | undefined
To explicitly allow undefined as a return value:
// Explicitly allow undefined
const result = await flow<string | undefined>({
async task1() {
const data = await getData()
if (!data) this.$end(undefined) // ✅ OK: undefined is in the type parameter
this.$end(data)
}
})
// result: string | undefined
⚠️ Important Notes:
- The type parameter
<R>is required and specifies what type$end()accepts - If you want to call
$end(undefined), you must explicitly includeundefinedinR(e.g.,flow<undefined>orflow<string | undefined>) - If no task calls
this.$end(), the flow will returnundefined - Once
$end()is called, subsequent dependency accesses will fail (but are caught silently) $end()stops the current task execution (throws internally)
Development
pnpm install # Install dependencies
pnpm test # Run tests
pnpm build # Build
Author
Shu Ding
License
MIT
Related repositories
Master programming by recreating your favorite technologies from scratch.
A curated meta-list of curated lists organized by technology domain. The repository acts as a directory pointing to hundreds of specialized awesome lists covering programming languages, platforms, frameworks, and tooling. All content is community-contributed under the CC0 public domain dedication.
Public APIs is a community-curated GitHub repository listing free, publicly accessible APIs across a wide range of categories, with auth type, HTTPS, and CORS noted for each entry. It's a browsable reference, not a library to install.
freeCodeCamp is a free, self-paced curriculum for learning to code, published as open source at freeCodeCamp/freeCodeCamp. It's a 501(c)(3) nonprofit funded by donor support, structured around six certifications in its Full-Stack Developer Curriculum, each gated by required projects instead of open-book quizzes. The repository also carries beta language certifications for developers, interview-prep resources, and the code that runs the live freecodecamp.org platform.
Quick answers
Does shuding/better-all have a project website?
No homepage URL was recorded for shuding/better-all in TopGit's last sync. The README tab above frequently contains screenshots and demo links, or check the repository description on GitHub.
Does shuding/better-all have any tags?
TopGit's last sync did not record any GitHub topics for shuding/better-all. GitHub topics appear in the right sidebar of a repository page; that's the authoritative place to check.
How active is development on shuding/better-all?
The most recent commit recorded on shuding/better-all was 27 days ago, based on the GitHub push timestamp. The repository has 20 forks — one of the better signals of community interest.
How many stars does shuding/better-all have?
shuding/better-all has 1.1k GitHub stars — refresh the page for the live number, or check github.com/shuding/better-all. TopGit mirrors GitHub's count but does not claim minute-by-minute accuracy.
What language is shuding/better-all written in?
shuding/better-all is written primarily in TypeScript. GitHub's language field is based on the largest share of bytes in the default branch.
What license does shuding/better-all use?
shuding/better-all is released under the MIT license. Always verify the LICENSE file directly on GitHub for the authoritative terms — license strings can be edited out of sync with a project's actual stance.
Where do I read more about shuding/better-all?
This TopGit page is a snapshot — the READ ME tab shows the project's own README content (links stripped, images preserved). The GitHub repository at github.com/shuding/better-all is the definitive source.
Read full README in the tab above.
Still deciding about better-all?
One click hands the question to an AI along with this page — see what it says about better-all.