Promise.all with automatic dependency optimization and full type inference.
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 secondsYou 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 secondsBut 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 secondsTo 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.
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.
npm install better-all
# or
pnpm add better-all
# or
bun add better-all
# or
yarn add better-all- 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
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)
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
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
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 }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 parallelFull 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 // stringconst { 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 })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.
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)
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
}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
}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' }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.
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
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
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))
}
})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 firstconst 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 })
}
})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 | undefinedTo 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- 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)
pnpm install # Install dependencies
pnpm test # Run tests
pnpm build # BuildMIT