Skip to content

Upgrade from 4.x to 5.0

How-to. A checklist for moving an existing codebase from unthrown@4.x to 5.0. Most of it is mechanical renames the compiler will point at; two changes are worth doing deliberately. Work top to bottom.

At a glance

Kind4.x5.0
Renamed (loud)mapErr / flatMapErr / recoverErr / tapErr / flatTapErrmapErrCases / flatMapErrCases / recoverErrCases / tapErrCases / flatTapErrCases
Renamed (loud)match({ ok, err, defect })match({ ok, errCases, defect })
Renamed exportUnwrapErrorGetError
Removed (aliases)unwrap / unwrapErr / unwrapOr / unwrapOrElseget / getErr / getOr / getOrElse
Removed (aliases)orElse / recoverflatMapErrCases / recoverErrCases
RemovedmatchTags / the TagHandlers typematch({ …, errCases: (m) => m.with(P.tag("…"), …) })
Renamed (loud)the standalone tag("…") exportP.tag("…") — it lives in the pattern namespace with every other constructor
Changed, same namegetOrThrow() on any Resultgated to a non-empty error channel (use get() otherwise)
Packagingts-pattern bundled as a dependencythe matcher is built-in — no dependency at all
Packaging@unthrown/pattern (match / P / tag)absorbed into core — import them from unthrown
Newensure, DoAsync, and the built-in match / P / NonExhaustiveError

Everything except the two rows below is a compile error at the old call site, so pnpm typecheck is your migration to-do list.

1. Nothing to install — the matcher is built-in

unthrown's exhaustive matcher is its own module (same .with(…) / P call-site shape ts-pattern users know), exported as match / P / NonExhaustiveError. There is no dependency to install alongside unthrown — and no version of any third-party library can change what "exhaustive" means.

(History, if you followed the betas: early v5 bundled ts-pattern, then briefly declared it a peer; the built-in matcher replaced it. If you added ts-pattern for unthrown's sake, you can remove it — unless your own code imports it directly for other matching.)

One boundary to know: patterns built by the real ts-pattern library are not accepted by unthrown's matchers (and vice versa). Import P / match from "unthrown" at unthrown call sites; keep ts-pattern for your own unrelated matching if you use it.

2. Rename the error-channel combinators

Every Err-channel combinator gained a …Cases suffix, because each takes a matcher over the error's cases, not a plain (error) => … callback. The old names no longer exist, so the compiler flags each site:

diff
- result.mapErr((m) => m.with(tag("NotFound"), wrap))
+ result.mapErrCases((m) => m.with(P.tag("NotFound"), wrap))

The arm moved too — tag(…) is now P.tag(…), a separate change covered in §7.

Same for flatMapErrflatMapErrCases, recoverErrrecoverErrCases, tapErrtapErrCases, flatTapErrflatTapErrCases. See Exhaustive error matching for why.

3. Rename match's error handler — errerrCases

The one break the compiler almost missed

In 5.0 the match error handler receives the matcher, not the error value, and its key is renamed errerrCases to match the combinators. That rename is what makes the change loud: a leftover 4.x err: (error) => … handler still satisfied the new matcher constraint whenever it threw (a throwing handler returns never, which vacuously satisfies it), so it compiled and then threw the matcher object at runtime. The renamed key turns that into an excess-property compile error instead.

diff
  result.match({
    ok: (value) => value,
-   err: (error) => handleError(error),
+   errCases: (matcher) => matcher.with(P._, (error) => handleError(error)),
    defect: (cause) => report(cause),
  })

The P._ arm above is the mechanical port — a 4.x err callback was a blanket handler, and one wildcard reproduces it exactly. Treat it as a way station, not a destination: replace it with one arm per case in E (grouping those that share a handler) so the site starts failing the build when the union grows. That is the whole reason the handler changed shape. @unthrown/oxlint's no-catch-all-pattern, now in the recommended preset, will point at every arm still waiting to be converted.

If your old err handler was (error) => { throw error }, prefer getOrThrow() — the sanctioned single-throw extractor — over re-throwing inside a match arm.

4. Replace the removed aliases

The @deprecated aliases from the 4.x line are gone (one concept, one name):

RemovedUse
unwrapget
unwrapErrgetErr
unwrapOrgetOr
unwrapOrElsegetOrElse
orElseflatMapErrCases
recoverrecoverErrCases
matchTagsmatch({ …, errCases: (m) => m.with(P.tag("…"), …) })

UnwrapError (the throw type of get/getErr) is renamed GetError.

5. Check the two behavioural gates

  • getOrThrow() now compiles only when the error channel is non-empty. On a Result<T, never> there is nothing to throw, so the compiler steers you to get(). The diagnostic names the reason.
  • get() / getErr() are unchanged, but if you were relying on getOrThrow as a universal extractor, split by channel state: get() when E = never, getOrThrow() when it isn't.

6. @unthrown/pattern is gone

match and P are now exported by unthrown itself, and the package's tag helper is now P.tag (see the next section). Drop the @unthrown/pattern dependency and re-point the imports:

diff
- import { match, P, tag } from "@unthrown/pattern";
+ import { match, P } from "unthrown";

7. tag moved onto P

tag(t) builds the { _tag: t } pattern — it is a pattern constructor, so it now lives in the pattern namespace beside P._, P.instanceOf, P.when and the rest. The standalone export is removed, not deprecated: one concept, one spelling.

diff
- import { tag } from "unthrown";
+ import { P } from "unthrown";

  result.mapErrCases((matcher) =>
    matcher
-     .with(tag("NotFound"), () => 404)
-     .with(tag("Conflict"), () => 409),
+     .with(P.tag("NotFound"), () => 404)
+     .with(P.tag("Conflict"), () => 409),
  );

The missing tag export is a compile error at every import site, and each call site follows from it. Nothing about the pattern's behaviour changed.

What you gained

  • ensure — validate or refine a success in place (Result<T, E | E2>, or a type-guard narrowing T).
  • DoAsync() — the pre-lifted async twin of Do().
  • match / P as first-class re-exports, so the error matcher is one import.

Released under the MIT License.