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 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.
Promise.allThis 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
this.$signalall(tasks, options?)Execute tasks with automatic dependency resolution.
tasks: Object of async task functionsoptions: Optional configuration object
debug: Set to true to output a waterfall chart showing task execution timelinesignal: An AbortSignal to abort all tasks externallythis.$ - an object with promises for all task resultsthis.$signal - an AbortSignal that aborts when any sibling task failsPromise.all)allSettled(tasks, options?)Execute tasks with automatic dependency resolution, returning settled results for all tasks.
tasks: Object of async task functionsoptions: Optional configuration object
debug: Set to true to output a waterfall chart showing task execution timelinesignal: An AbortSignal to abort all tasks externallythis.$ - an object with promises for all task resultsthis.$signal - an AbortSignal (only aborts on external signal, not on sibling failure){ status: 'fulfilled', value } or { status: 'rejected', reason }Promise.allSettled)flow<R>(tasks, options?)Execute tasks with automatic dependency resolution and early exit support.
<R>: Required. Specifies the return type that $end() must accepttasks: Object of async task functionsoptions: Same as all() - optional configuration objectthis.$ - an object with promises for all task resultsthis.$signal - an AbortSignal for resource cleanupthis.$end(value: R) - function to exit the entire flow early with a return value of type RR | undefined
$end() callundefined if no task calls $end()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 parallel
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
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 })
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:
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:
This makes it easy to:
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
}
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
}
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' }
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:
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())this.$end(value) determines the return value$end() is called, other tasks that try to access dependencies will receive errors (caught silently)$end) still propagate to the callerthis.$signal for resource cleanupimport { 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 first
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 })
}
})
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:
<R> is required and specifies what type $end() accepts$end(undefined), you must explicitly include undefined in R (e.g., flow<undefined> or flow<string | undefined>)this.$end(), the flow will return undefined$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 # Build
MIT
TypeScript
100.0%
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 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.
Promise.allThis 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
this.$signalall(tasks, options?)Execute tasks with automatic dependency resolution.
tasks: Object of async task functionsoptions: Optional configuration object
debug: Set to true to output a waterfall chart showing task execution timelinesignal: An AbortSignal to abort all tasks externallythis.$ - an object with promises for all task resultsthis.$signal - an AbortSignal that aborts when any sibling task failsPromise.all)allSettled(tasks, options?)Execute tasks with automatic dependency resolution, returning settled results for all tasks.
tasks: Object of async task functionsoptions: Optional configuration object
debug: Set to true to output a waterfall chart showing task execution timelinesignal: An AbortSignal to abort all tasks externallythis.$ - an object with promises for all task resultsthis.$signal - an AbortSignal (only aborts on external signal, not on sibling failure){ status: 'fulfilled', value } or { status: 'rejected', reason }Promise.allSettled)flow<R>(tasks, options?)Execute tasks with automatic dependency resolution and early exit support.
<R>: Required. Specifies the return type that $end() must accepttasks: Object of async task functionsoptions: Same as all() - optional configuration objectthis.$ - an object with promises for all task resultsthis.$signal - an AbortSignal for resource cleanupthis.$end(value: R) - function to exit the entire flow early with a return value of type RR | undefined
$end() callundefined if no task calls $end()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 parallel
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
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 })
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:
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:
This makes it easy to:
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
}
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
}
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' }
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:
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())this.$end(value) determines the return value$end() is called, other tasks that try to access dependencies will receive errors (caught silently)$end) still propagate to the callerthis.$signal for resource cleanupimport { 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 first
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 })
}
})
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:
<R> is required and specifies what type $end() accepts$end(undefined), you must explicitly include undefined in R (e.g., flow<undefined> or flow<string | undefined>)this.$end(), the flow will return undefined$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 # Build
MIT
TypeScript
100.0%