Combine parallel results
How-to. Collect several independent
Results into one. For dependent steps (each needs the previous value), use do-notation instead.
Combine an array with all
all collects a tuple/array of Results into a Result of all their values. The first Err short-circuits; any Defect dominates (even over an earlier Err). A fixed tuple keeps its positional types; a dynamic Result<T, E>[] collapses to Result<T[], E>:
import { all, Ok, type Result } from "unthrown";
all([Ok(1), Ok("two"), Ok(true)]).get(); // => [1, "two", true] (typed [number, string, boolean])
all([Ok(1), Ok(2)] as Result<number, never>[]).get(); // => number[]Combine a record with allFromDict
For named parallel work, allFromDict takes a record instead — same rules, no tupling:
import { allFromDict, Ok } from "unthrown";
allFromDict({ id: Ok(1), name: Ok("ada") }).get(); // => { id: 1, name: "ada" }Both report the first Err, unless a later input carries a Defect — the walk keeps looking so a Defect still takes precedence. To report every error instead, see Accumulate every error below.
Combine async results concurrently
allAsync and allFromDictAsync are the asynchronous counterparts — same folding rules, inputs resolved concurrently (order preserved), and (like every AsyncResult) they never reject:
import { allAsync } from "unthrown";
// loadProfile / loadPosts / loadFollowers each return an AsyncResult
const page = allAsync([loadProfile(id), loadPosts(id), loadFollowers(id)]);
// AsyncResult<[Profile, Post[], User[]], ProfileError>
page.map(([profile, posts, followers]) =>
renderPage(profile, posts, followers),
);Use allFromDictAsync to key the concurrent results by name instead of position.
Accumulate every error with validateAll
all tells you the first thing that went wrong. When you are running independent checks — business rules that don't depend on one another — you usually want all of them:
import { validateAll } from "unthrown";
// each check returns Result<_, OutOfStock | OverLimit | UnservedZone>
validateAll(
[checkStock(order), checkCreditLimit(customer), checkShippingZone(address)],
(violations) => new OrderRejected({ violations }),
);
// Result<[StockHold, CreditLine, Zone], OrderRejected>merge is mandatory, and that is the design. neverthrow's combineWithAllErrors hands you an E[]; unthrown makes you name an E2, because an array of errors is a shape, not a domain failure — and E holds anticipated domain failures (why). You decide what "three rules were violated" means in your domain, once, at the point you collect them.
Four things worth knowing:
mergeis total. It receives a non-empty list, and it is called only when at least oneErrwas collected — never on an all-Okinput. No "shouldn't happen" branch.- A
Defectstill dominates, and the accumulated errors are discarded without reachingmerge. A defect means something in the batch failed in a way nobody modeled, so the violations computed alongside it aren't trustworthy. - A throw inside
mergebecomes aDefect, like every other callback in the library. mergemust be synchronous. Anasyncone is a compile error — itsPromisewould land unqualified inE.
The record form names each failure, and the entries are correlated per key, so a switch on the key narrows the error:
import { validateAllFromDict } from "unthrown";
validateAllFromDict(
{
vatRate: checkRate(inv),
currency: checkCurrency(inv),
dueDate: checkDueDate(inv),
},
(entries) => new InvoiceRejected({ fields: entries.map(([field]) => field) }),
);That correlation is what keeps two checks that share an error type distinguishable — with a flat list of RuleViolateds you could not tell which field produced which.
validateAllAsync and validateAllFromDictAsync are the concurrent counterparts. Note that the accumulating and fail-fast forms do the same work: every input has already run by the time the aggregate sees it, so the choice is purely about which errors get reported.
Schema-shaped input?
If you're validating a request body or a form, reach for fromSchema instead — a validator already hands you every issue as the modeled error. validateAll is for independent checks you wrote yourself.
Where to go next
- Sequence dependent steps: Sequence dependent steps.
- The full aggregate surface: Result & AsyncResult surface.