Skip to content

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>:

ts
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:

ts
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:

ts
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:

ts
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:

  • merge is total. It receives a non-empty list, and it is called only when at least one Err was collected — never on an all-Ok input. No "shouldn't happen" branch.
  • A Defect still dominates, and the accumulated errors are discarded without reaching merge. 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 merge becomes a Defect, like every other callback in the library.
  • merge must be synchronous. An async one is a compile error — its Promise would land unqualified in E.

The record form names each failure, and the entries are correlated per key, so a switch on the key narrows the error:

ts
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

Released under the MIT License.