{"_id":"@dmytromykhailiuk/typed-error-execution","_rev":"4-e27f827a4241f2a66b11b23bcde39a0a","name":"@dmytromykhailiuk/typed-error-execution","dist-tags":{"latest":"2.0.1"},"versions":{"1.0.0":{"name":"@dmytromykhailiuk/typed-error-execution","version":"1.0.0","keywords":["result","result-type","either","typed-errors","tagged-error","tagged-union","error-handling","railway-oriented","functional","neverthrow","effect","no-throw","typescript","typed","async","zero-dependencies"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/typed-error-execution@1.0.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/typed-error-execution#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/typed-error-execution/issues"},"dist":{"shasum":"984829877d9531cedbb08509ea68eb55052ca612","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/typed-error-execution/-/typed-error-execution-1.0.0.tgz","fileCount":9,"integrity":"sha512-nuMFs2MA6qnWDpNNsqwlRdr8RcXfE1aaqoym+G0fC+qChAteCBCgZaRpwol7GcASiM9/v/N0x7j8EE8l/axneA==","signatures":[{"sig":"MEQCIBZSdNBq181X1WkR5n8LfUv43nVwIIO5dUoUj2aL1pr2AiA9HZZ3F8aBP+6wlTCkReaO7Y8RYAri9PEJBHiznHOSzQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":190915},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"5d996ddcc76948d201fae5d3a673c96fe351945a","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","verify":"npm run lint && npm run typecheck && npm run test && npm run build","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"tsx playground/index.ts","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build","playground:watch":"tsx watch playground/index.ts"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/typed-error-execution.git","type":"git"},"_npmVersion":"11.6.2","description":"Typed, tagged errors for TypeScript. A Result type that tracks every failure a call can produce in its signature — inspired by Effect and neverthrow, but small and boring on purpose.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.1","tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4","@vitest/coverage-v8":"^2.1.8"},"_npmOperationalInternal":{"tmp":"tmp/typed-error-execution_1.0.0_1784669672588_0.1192910084645522","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@dmytromykhailiuk/typed-error-execution","version":"1.1.0","keywords":["result","result-type","either","typed-errors","tagged-error","tagged-union","error-handling","railway-oriented","functional","neverthrow","effect","no-throw","typescript","typed","async","zero-dependencies"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/typed-error-execution@1.1.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/typed-error-execution#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/typed-error-execution/issues"},"dist":{"shasum":"83cde459e5b2bbfec55a44234d6259fdabfa6817","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/typed-error-execution/-/typed-error-execution-1.1.0.tgz","fileCount":9,"integrity":"sha512-kqiHdfZScNgJl84CDZIQ4JuMfpffhGNaxEMZ2Gyx3heg9RA1spxmKdKquklort5l7tGp2bgWXcNx9sbwK24Qng==","signatures":[{"sig":"MEYCIQC11Qjqw2uYdRbOKebgCKudkHqFqWBWBXy9V0csc9QZbAIhAK4ysGhE2cuzon7jGPPqpjp3MIhoy+mIJXVGyW5hRSwY","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":197033},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"187225c16dc6b7aaa149a0f5fbef2ae8ecb93bd4","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","verify":"npm run lint && npm run typecheck && npm run test && npm run build","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"tsx playground/index.ts","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build","playground:watch":"tsx watch playground/index.ts"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/typed-error-execution.git","type":"git"},"_npmVersion":"11.6.2","description":"Typed, tagged errors for TypeScript. A Result type that tracks every failure a call can produce in its signature — inspired by Effect and neverthrow, but small and boring on purpose.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.1","tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4","@vitest/coverage-v8":"^2.1.8"},"_npmOperationalInternal":{"tmp":"tmp/typed-error-execution_1.1.0_1786485566348_0.7455216862101688","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@dmytromykhailiuk/typed-error-execution","version":"2.0.0","keywords":["result","result-type","either","typed-errors","tagged-error","tagged-union","error-handling","railway-oriented","functional","neverthrow","effect","no-throw","typescript","typed","async","zero-dependencies"],"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","_id":"@dmytromykhailiuk/typed-error-execution@2.0.0","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"homepage":"https://github.com/dmytromykhailiuk/typed-error-execution#readme","bugs":{"url":"https://github.com/dmytromykhailiuk/typed-error-execution/issues"},"dist":{"shasum":"6aea740839912ce67a8931c381a23a4a466de0d1","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/typed-error-execution/-/typed-error-execution-2.0.0.tgz","fileCount":9,"integrity":"sha512-qkjgK92iq8b2+keo6Ta23HU7mYlI4J0x7mOOC9y9FokbQMZh+xKwghc8FG3r2es8xfIROeshHYrOi90eH5OMDg==","signatures":[{"sig":"MEUCIBNnesA+Ya12CWPQ9zXePl2de8t807+EprtImI0QojWgAiEAyV4qRLHaDJJJrn/n8Pce0YJybpwkteMrcpZMy8WwyMI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":256166},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"d30863a472a6593ee04397cf501af81052a8a579","scripts":{"dev":"tsup --watch","lint":"biome check .","test":"vitest run","build":"tsup","format":"biome format --write .","verify":"npm run lint && npm run typecheck && npm run test && npm run build","lint:fix":"biome check --write .","typecheck":"tsc --noEmit","playground":"tsx playground/index.ts","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build","playground:watch":"tsx watch playground/index.ts"},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"repository":{"url":"git+https://github.com/dmytromykhailiuk/typed-error-execution.git","type":"git"},"_npmVersion":"11.6.2","description":"Typed, tagged errors for TypeScript. A Result type that tracks every failure a call can produce in its signature — inspired by Effect and neverthrow, but small and boring on purpose.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.1","tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.7.3","@types/node":"^22.10.5","@biomejs/biome":"^1.9.4","@vitest/coverage-v8":"^2.1.8"},"_npmOperationalInternal":{"tmp":"tmp/typed-error-execution_2.0.0_1786547425191_0.20535523391493626","host":"s3://npm-registry-packages-npm-production"}},"2.0.1":{"name":"@dmytromykhailiuk/typed-error-execution","version":"2.0.1","description":"Typed, tagged errors for TypeScript. A Result type that tracks every failure a call can produce in its signature and subtracts each one as you handle it, plus branded types for values that must not be interchangeable — inspired by Effect and neverthrow, b","type":"module","sideEffects":false,"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","keywords":["result","result-type","result-pattern","either","error-handling","errors-as-values","error-as-value","typed-errors","type-safe-errors","tagged-error","tagged-union","discriminated-union","exhaustiveness","no-throw","try-catch","railway-oriented","railway","functional","functional-programming","fp","monad","branded-types","branded","brand","nominal-typing","opaque-types","neverthrow","effect","effect-ts","ts-results","typescript","ts","typed","type-safe","async","promise","esm","cjs","tree-shakeable","zero-dependencies"],"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","playground":"tsx playground/index.ts","playground:watch":"tsx watch playground/index.ts","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","lint":"biome check .","lint:fix":"biome check --write .","format":"biome format --write .","verify":"npm run lint && npm run typecheck && npm run test && npm run build","prepublishOnly":"npm run build"},"engines":{"node":">=18"},"devDependencies":{"@biomejs/biome":"^1.9.4","@types/node":"^22.10.5","@vitest/coverage-v8":"^2.1.8","tsup":"^8.3.5","tsx":"^4.23.1","typescript":"^5.7.3","vitest":"^2.1.8"},"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/typed-error-execution.git"},"bugs":{"url":"https://github.com/dmytromykhailiuk/typed-error-execution/issues"},"homepage":"https://dmytromykhailiuk.github.io/typed-error-execution/","gitHead":"4b01de85425bfff6d927460282f59fd21139bc14","_id":"@dmytromykhailiuk/typed-error-execution@2.0.1","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-nl1swf9TSMGAz593ZuOvdyGI9kP9X86cIlc5h+ZuA3kKjyF68bWcyBVejjDXiTE6OnmlqguJCCdfD/PS+wiD0w==","shasum":"01e86c7fb23a6ab0707c01682a5123d61e4f8a55","tarball":"https://registry.npmjs.org/@dmytromykhailiuk/typed-error-execution/-/typed-error-execution-2.0.1.tgz","fileCount":9,"unpackedSize":257114,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIHwPLTJJ3X43bWUotyHTH6Yu3ne6Yj2BlMR019ZQDuI9AiEAvD9eynxx5Gh3LYFLU6XeMLBOyDbXMTwvTRswNQDpq8U="}]},"_npmUser":{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"},"directories":{},"maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/typed-error-execution_2.0.1_1786639463080_0.7689456298586954"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-21T21:34:32.408Z","modified":"2026-08-13T16:44:23.406Z","1.0.0":"2026-07-21T21:34:32.756Z","1.1.0":"2026-08-11T21:59:26.533Z","2.0.0":"2026-08-12T15:10:25.347Z","2.0.1":"2026-08-13T16:44:23.216Z"},"bugs":{"url":"https://github.com/dmytromykhailiuk/typed-error-execution/issues"},"author":{"name":"Dmytro Mykhailiuk","email":"dimamykhayluk@gmail.com"},"license":"MIT","homepage":"https://dmytromykhailiuk.github.io/typed-error-execution/","keywords":["result","result-type","result-pattern","either","error-handling","errors-as-values","error-as-value","typed-errors","type-safe-errors","tagged-error","tagged-union","discriminated-union","exhaustiveness","no-throw","try-catch","railway-oriented","railway","functional","functional-programming","fp","monad","branded-types","branded","brand","nominal-typing","opaque-types","neverthrow","effect","effect-ts","ts-results","typescript","ts","typed","type-safe","async","promise","esm","cjs","tree-shakeable","zero-dependencies"],"repository":{"type":"git","url":"git+https://github.com/dmytromykhailiuk/typed-error-execution.git"},"description":"Typed, tagged errors for TypeScript. A Result type that tracks every failure a call can produce in its signature and subtracts each one as you handle it, plus branded types for values that must not be interchangeable — inspired by Effect and neverthrow, b","maintainers":[{"name":"dmytromykhailiuk","email":"dimamykhayluk@gmail.com"}],"readme":"# typed-error-execution\n\n> **Full documentation:** open [Docs](https://dmytromykhailiuk.github.io/typed-error-execution/)\n\n**Errors that live in the type system.**\n\nA `Result<T, E>` either succeeded with a `T` or failed with one of the tagged errors in `E`. Errors are ordinary classes carrying a literal `_tag`, so TypeScript tracks exactly which failures a call can still produce — and `handleError()` removes them from that union one at a time until nothing is left.\n\nInspired by [Effect](https://effect.website/) and [neverthrow](https://github.com/supermacro/neverthrow), but deliberately small: **six runtime exports**, one API for sync and async, no generators, no runtime, no fibers. Zero dependencies.\n\n```ts\nimport {\n  Result,\n  Tagged,\n  TaggedError,\n} from \"@dmytromykhailiuk/typed-error-execution\";\n\nclass ValidationFailed extends Tagged(\"ValidationFailed\") {\n  constructor(readonly field: string, readonly reason: string) {\n    super();\n  }\n}\nclass EmailAlreadyRegistered extends Tagged(\"EmailAlreadyRegistered\") {\n  constructor(readonly email: string) {\n    super();\n  }\n}\n// An infrastructure failure extends the native Error, so it has a stack.\nclass DatabaseUnavailable extends TaggedError(\"DatabaseUnavailable\") {}\n\nconst registerUser = Result.registerExecution(async (input: SignUpInput) => {\n  if (input.password.length < 12) {\n    return Result.err(\n      new ValidationFailed(\"password\", \"must be at least 12 characters\")\n    );\n  }\n\n  const existing = await db.users.findByEmail(input.email);\n  if (existing) return Result.err(new EmailAlreadyRegistered(input.email));\n\n  return Result.ok(await db.users.insert(input));\n});\n// (input: SignUpInput) => AsyncResult<User, ValidationFailed | EmailAlreadyRegistered>\n\nconst settled = await registerUser(req.body)\n  .mapValue((user) => Result.ok({ status: 201, body: publicProfile(user) }))\n  .handleError(ValidationFailed, (e) =>\n    Result.ok({ status: 422, body: { field: e.field, reason: e.reason } })\n  )\n  .handleError(EmailAlreadyRegistered, () =>\n    Result.ok({ status: 409, body: { error: \"that email is already in use\" } })\n  )\n  .getResult(); // Result<Response, never> — every failure is now a response\n\nconst response = settled.unwrap(); // safe, and the compiler knows it\n```\n\n---\n\n## Contents\n\n- [Install](#install)\n- [Why](#why)\n- [Defining errors](#defining-errors)\n- [Branding values](#branding-values)\n- [Creating results](#creating-results)\n- [One method, sync or async](#one-method-sync-or-async)\n- [Registering executions](#registering-executions)\n- [Transforming](#transforming)\n- [Handling errors](#handling-errors)\n- [Asynchronous chains](#asynchronous-chains)\n- [Combining results](#combining-results)\n- [Collecting every error](#collecting-every-error)\n- [Getting the value out](#getting-the-value-out)\n- [Bridging code that throws](#bridging-code-that-throws)\n- [Recipes](#recipes)\n- [TypeScript notes](#typescript-notes)\n- [API reference](#api-reference)\n- [Comparison](#comparison)\n- [Development](#development)\n\n---\n\n## Install\n\n```sh\nnpm i @dmytromykhailiuk/typed-error-execution\n```\n\nNo peer dependencies. TypeScript **5.0+** is required (the `const` type parameters used by `all()` landed in 5.0). ESM and CJS builds are both published, with separate `.d.ts` / `.d.cts` declarations.\n\n### The whole surface\n\n```ts\nimport {\n  Result, // the type, and every static that builds or combines one\n  Tagged, // base class factory for domain errors, and for families of them\n  TaggedError, // the same, but extending the native Error\n  ResultUnwrapError, // what unwrap() throws\n  brand, // labels for values that must not be interchangeable\n  InvalidBrand, // what a checked brand reports\n  type Branded, // the same label, as a type\n  type BrandOf, // the type a brand() factory produces\n  type Brand, // the shape of a brand() factory itself\n  type CheckedBrand, // the same, with is() and safe() on it\n} from \"@dmytromykhailiuk/typed-error-execution\";\n```\n\nEverything else lives on `Result`. There is no `AsyncResult` to import — the asynchronous half of a chain is produced for you and inferred at every step, so you use it constantly without ever naming it.\n\n---\n\n## Why\n\nA thrown error is invisible to the type system. `async function registerUser(input: SignUpInput): Promise<User>` tells you nothing about the five ways it can fail — a uniqueness check, a password policy, a database write, a call to a payment provider — and nothing breaks when a sixth is added.\n\nThis library makes failure part of the return type:\n\n```ts\nfunction registerUser(\n  input: SignUpInput\n): AsyncResult<\n  User,\n  ValidationFailed | EmailAlreadyRegistered | DatabaseUnavailable\n>;\n```\n\nThree properties follow from that, and they are the whole point:\n\n1. **You cannot forget a failure.** The union is in the signature. Adding a new error to a function surfaces as a type change at every call site.\n2. **Handling is subtraction.** Every `handleError()` removes exactly the classes you named. When the union reaches `never`, the compiler knows the value is safe.\n3. **Nothing is magic.** Errors are `class` instances. Matching is `instanceof`. A `Result` is a two-field object. You can read the entire implementation in one sitting.\n\n### What it deliberately does not do\n\nNo effect system, no dependency injection, no generator syntax, no retry/schedule combinators, no runtime to install. If you need those, use Effect — it is excellent and this is not trying to replace it. This is the layer below: typed errors and nothing else.\n\n---\n\n## Defining errors\n\nAn error is a class extending `Tagged(tag)`. The tag is a **string literal** — that literal is what makes the union in `Result<T, E>` meaningful.\n\n```ts\nclass UserNotFound extends Tagged(\"UserNotFound\") {\n  constructor(readonly userId: string) {\n    super();\n  }\n}\n\nclass SubscriptionRequired extends Tagged(\"SubscriptionRequired\") {\n  constructor(readonly currentPlan: string, readonly requiredPlan: string) {\n    super();\n  }\n}\n\nclass RateLimited extends Tagged(\"RateLimited\") {\n  constructor(readonly retryAfterSeconds: number) {\n    super();\n  }\n}\n```\n\nErrors carry whatever data you give them. That data survives the whole chain and is fully typed inside a handler.\n\n### `Tagged` vs `TaggedError`\n\n|                     | `Tagged`        | `TaggedError`                      |\n| ------------------- | --------------- | ---------------------------------- |\n| Extends `Error`     | no              | yes                                |\n| `message` / `stack` | no              | yes                                |\n| Cost to construct   | a plain object  | captures a stack trace             |\n| Use for             | domain outcomes | things you log, report, or `throw` |\n\n```ts\nclass DatabaseUnavailable extends TaggedError(\"DatabaseUnavailable\") {}\nclass PaymentGatewayError extends TaggedError(\"PaymentGatewayError\") {}\n\nconst err = new DatabaseUnavailable(\"connection pool exhausted after 5000ms\");\nerr._tag; // \"DatabaseUnavailable\"\nerr.name; // \"DatabaseUnavailable\"\nerr.message; // \"connection pool exhausted after 5000ms\"\nerr.stack; // a real stack trace\nerr instanceof Error; // true — Sentry, pino and friends handle it correctly\n\nlogger.error({ err }, \"query failed\"); // serialises like any other Error\n```\n\nCapturing a stack trace is by far the most expensive part of creating an error. If a failure is an ordinary control-flow outcome — \"this user does not exist\" — `Tagged` keeps it as cheap as returning a value, which is what it is.\n\n> **Each call to** `Tagged()` **returns a distinct class.** Two error types that happen to share a tag string are still separate under `instanceof`, so `handleError` will not confuse them at runtime. In the type system they are indistinguishable — nothing tells two identical shapes apart — so give each failure its own tag.\n\n### Error inheritance\n\nErrors inherit from one another: pass the parent class as the second argument. A class and everything below it form a **family**. The tag becomes a dotted path, and that path is what carries the lineage into the type system.\n\n```ts\nclass PaymentError extends Tagged(\"PaymentError\") {}\nclass Declined extends Tagged(\"Declined\", PaymentError) {}\nclass CardExpired extends Tagged(\"CardExpired\", Declined) {}\nclass Rejected extends Tagged(\"Rejected\", PaymentError) {}\n\nnew CardExpired()._tag; // \"PaymentError.Declined.CardExpired\"\nnew CardExpired() instanceof PaymentError; // true\n```\n\n`handleError` handles a class **and everything below it**, at any depth:\n\n| Handled class    | Also handles                                | Leaves        |\n| ---------------- | ------------------------------------------- | ------------- |\n| `PaymentError`   | `Declined`, `CardExpired`, `Rejected`       | —             |\n| `Declined`       | `CardExpired`                               | `Rejected`    |\n| `CardExpired`    | —                                           | `Rejected`    |\n\n```ts\ncharge(order) // AsyncResult<Receipt, CardExpired | Rejected>\n  .handleError(Declined, (e) => Result.ok(retry(e))) // CardExpired too\n  .handleError(Rejected, () => Result.ok(abandon()));\n// AsyncResult<Receipt, never>\n```\n\nNesting is unlimited, and each level is a **distinct type**. That is the whole reason the second argument exists — a plain `class Child extends Parent {}` adds nothing, so it is structurally identical to its parent, and TypeScript collapses `Child1 | Child2` into one member. The error union then silently stops tracking which failure it is holding:\n\n```ts\nclass Random1Error extends RandomError {} // ✗ same type as RandomError\nclass Random2Error extends RandomError {} // ✗ same type again\n// Result<number, Random1Error | Random2Error>  →  Result<number, Random1Error>\n\nclass Random1Error extends Tagged(\"Random1Error\", RandomError) {} // ✓ its own type\nclass Random2Error extends Tagged(\"Random2Error\", RandomError) {} // ✓ its own type\n```\n\nA plain `extends` still behaves exactly as it always did — the child inherits the parent's tag and is handled by it — so nothing that already works breaks. It just cannot tell two children apart.\n\n> **A tag may not contain a dot.** The dot separates the levels of a family and `Tagged(tag, Parent)` composes it for you. Writing `Tagged(\"PaymentError.Faked\")` by hand is a compile error, because it would claim a lineage that `instanceof` does not back.\n\n`TaggedError` takes the same second argument, and `name` follows the composed path so a report says which member it was. `Error` can only enter a chain at its root: build the family with `TaggedError` at the top, and every level below is a real `Error` — including levels declared with plain `Tagged(tag, Parent)`.\n\n---\n\n## Branding values\n\nThe same idea as a tagged error, applied to ordinary values: a label that exists only in the type, so two things that are the same underneath stop being interchangeable.\n\n```ts\ntype UserId = Branded<string, \"userId\">;\ntype OrderId = Branded<string, \"orderId\">;\n\ndeclare function loadUser(id: UserId): User;\n\nloadUser(orderId); // ✗ the wrong id no longer compiles\n```\n\n`brand()` is the same thing as a factory, so the name is written once and you get a constructor with it:\n\n```ts\nconst UserId = brand(\"userId\");\ntype UserId = BrandOf<typeof UserId>; // Branded<string, \"userId\">\n\nloadUser(UserId(\"u_1\")); // ✓\nloadUser(\"u_1\"); //         ✗ a raw string is not a UserId\n```\n\nThe base type is `string` unless you say otherwise — `brand<\"orderNo\", number>(\"orderNo\")`, or by handing over a predicate, which is where it is normally inferred from.\n\nA branded value **is** its base type, so it goes anywhere a `string` is expected and keeps every method. Only the other direction is blocked, which is the whole point — there is nothing to unwrap.\n\n### Checked brands\n\nGive `brand()` a predicate and the label starts being earned rather than asserted:\n\n```ts\nconst Email = brand(\"email\", (value: string) => value.includes(\"@\"));\n\nEmail(\"user@example.com\"); // Branded<string, \"email\">\nEmail(\"nope\"); //            throws InvalidBrand\n\nif (Email.is(input)) sendWelcome(input); // input: Branded<string, \"email\">\n```\n\n`safe()` is the same check as a `Result`, so validation joins the chain instead of interrupting it:\n\n```ts\nEmail.safe(req.body.email) // Result<Branded<string, \"email\">, InvalidBrand>\n  .mapValue((email) => sendWelcome(email))\n  .handleError(InvalidBrand, (e) => Result.ok(reject(e.value)));\n```\n\n`InvalidBrand` is both a real `Error` and a tagged one, so it works in a `catch` and in `handleError` alike. It carries `brandName` and the `value` that was refused.\n\n> **Without a predicate there is no checker.** `brand(name)` gives you a constructor and nothing else — no `is`, no `safe`. A guard with no predicate behind it could only ever answer \"yes\", which is worse than not having one.\n\nNothing is added to the value at runtime: an unchecked brand is the identity function, and a checked one only runs your predicate.\n\n---\n\n## Creating results\n\n```ts\nResult.ok(user); // Result<User, never>\nResult.ok(); // Result<void, never>  — a command succeeded\nResult.empty(); // Result<null, never>  — nothing to return\nResult.err(new UserNotFound(\"u_8123\")); // Result<never, UserNotFound>\n```\n\n`ok()` and `empty()` differ in intent: `void` is the absence of a value, `null` is a value you can branch on. `empty()` reads well as the \"nothing to do, and that's fine\" branch of a handler.\n\n### The literal-tag rule\n\n`Result.err()` rejects an error whose `_tag` has widened to `string`, because a widened tag silently collapses the union and switches off error tracking:\n\n```ts\nclass HandRolled {\n  readonly _tag: string = \"HandRolled\"; // ← widened, not a literal\n}\n\nResult.err(new HandRolled());\n// Argument of type 'HandRolled' is not assignable to parameter of type\n// '{ ERROR: \"_tag must be a string literal — declare the class as\n//    `class X extends Tagged('X') {}`\" }'\n```\n\nExtending `Tagged()` always produces a literal, so in practice you never see this.\n\n---\n\n## One method, sync or async\n\nThere is no `mapValueAsync`, no `tryAsync`, no `registerAsyncExecution`. Every method takes a callback and looks at **what the callback actually returned**:\n\n- returns a **result** → the chain stays synchronous, and the value is readable on the next line;\n- returns a **promise or an async chain** → the rest of the chain is asynchronous, finished with `await` or `getResult()`.\n\n```ts\n// validating a request body: no I/O, so no promise anywhere\nconst plan = parsePlan(req.body.plan).unwrapOr(\"free\");\n\n// loading a user: I/O, so the chain is asynchronous from here on\nconst profile = await loadUser(userId)\n  .mapValue((user) => Result.ok(publicProfile(user)))\n  .getResult();\n\n// It is about the value, not the keyword — anything promise-shaped counts.\nResult.ok(userId).mapValue((id) => db.users.findById(id)); // async\nResult.ok(userId).mapValue((id) => loadUser(id)); // async: loadUser is async\n```\n\nThe check is `instanceof` on what came back — there are no heuristics applied to the function itself. The same rule governs every entry point:\n\n| Call                                    | Synchronous when              | Asynchronous when                               |\n| --------------------------------------- | ----------------------------- | ----------------------------------------------- |\n| `mapValue` · `mapError` · `handleError` | the callback returns a result | it returns a promise or an async chain          |\n| `tap` · `tapError`                      | the effect returns nothing    | the effect returns a promise (which is awaited) |\n| `Result.try` · `Result.fromThrowable`   | the body returns a value      | the body returns a promise                      |\n| `Result.registerExecution`              | the body returns a result     | the body is `async`                             |\n| `Result.all` · `Result.collect`         | every member is synchronous   | any member is asynchronous                      |\n\nTypes follow exactly the same rule, so the editor agrees with the runtime. A callback with one sync branch and one async branch counts as asynchronous — the safe reading.\n\n### Skipped steps\n\nA chain short-circuits: `Result.err(e).mapValue(fn)` never calls `fn`. There is no returned value to inspect, so the step reads the callback itself — an `async` function is identifiable at runtime, and the chain becomes a real asynchronous one even though nothing ran.\n\n```ts\nconst chain = Result.err(new UserNotFound(\"u_1\")).mapValue(async (u: User) =>\n  Result.ok(await enrich(u))\n);\n// an async chain, in the type and at runtime — enrich was never called\n\n// a synchronous callback keeps the step synchronous, so the value is right here\nResult.err(new UserNotFound(\"u_1\")).mapValue((u: User) => Result.ok(u.email))\n  .error;\n```\n\nDetection covers arrows, declarations, methods and bound functions.\n\n> **Reading the function is only needed when the step is skipped.** When it runs, the callback's returned value is checked with `instanceof` — exact, no heuristic — so a function that merely returns a promise is handled correctly there.\n\n```ts\nconst notAsync = (u: User) => enrich(u); // returns a promise, not declared async\n\n// Both lines are typed AsyncResult<Enriched, …> — the callback's signature says\n// so, and the type is the same either way. Only the runtime class differs.\nResult.ok(user).mapValue(notAsync);\n// runs    → the returned promise is seen → a real AsyncResult ✓\n\nResult.err(e).mapValue(notAsync);\n// skipped → nothing was returned → a Result, standing in for the chain\n```\n\nOn a skipped step there is no returned value to look at, and the callback cannot simply be called to find out: an errored result has no value to pass it, and running it would perform exactly the work the short-circuit exists to avoid. All that is left is the function object, which reveals `async` but not _returns a promise_.\n\nSo the gap is narrow: a skipped step whose callback is not declared `async` but would have returned a promise. Any `async` function passed through a wrapper lands here too, since a decorator or spy hands back an ordinary function.\n\nThat residual case is safe rather than merely tolerated: **every member the asynchronous type exposes works on a** `Result`. The chaining methods and terminals are shared outright, and awaiting a non-promise yields it unchanged. `getResult()` is the one that needs help, and `Result` carries it as a **private** method for exactly this reason — on the prototype, absent from the public API, because a synchronous result has nothing to resolve.\n\nThe guess only ever errs in the safe direction: a `Result` can stand in for a chain, but a chain cannot stand in for a `Result`, because reading `.value` off one would silently yield `undefined`. Both rules are pinned down by `tests/short-circuit.test.ts`.\n\n---\n\n## Registering executions\n\nLeft alone, TypeScript infers a function with several `return` branches as a **union of results**:\n\n```ts\nconst chargeSubscription = async (userId: string, cents: number) => {\n  const user = await db.users.findById(userId);\n  if (!user) return Result.err(new UserNotFound(userId));\n  if (!user.paymentMethodId) return Result.err(new NoPaymentMethod(userId));\n\n  const charge = await stripe.charges.create({\n    amount: cents,\n    customer: user.stripeId,\n  });\n  if (charge.status === \"failed\")\n    return Result.err(new PaymentDeclined(charge.failureCode));\n\n  return Result.ok(charge);\n};\n// Promise<Result<never, UserNotFound> | Result<never, NoPaymentMethod>\n//         | Result<never, PaymentDeclined> | Result<Charge, never>>\n```\n\nThat type is nearly unchainable. `registerExecution` collapses it into one result whose error parameter is the union — which is what you actually meant:\n\n```ts\nconst chargeSubscription = Result.registerExecution(\n  async (userId: string, cents: number) => {\n    const user = await db.users.findById(userId);\n    if (!user) return Result.err(new UserNotFound(userId));\n    if (!user.paymentMethodId) return Result.err(new NoPaymentMethod(userId));\n\n    const charge = await stripe.charges.create({\n      amount: cents,\n      customer: user.stripeId,\n    });\n    if (charge.status === \"failed\")\n      return Result.err(new PaymentDeclined(charge.failureCode));\n\n    return Result.ok(charge);\n  }\n);\n// (userId: string, cents: number)\n//   => AsyncResult<Charge, UserNotFound | NoPaymentMethod | PaymentDeclined>\n\n// A synchronous body needs no different call.\nconst parseWebhookEvent = Result.registerExecution((raw: unknown) => {\n  if (typeof raw !== \"object\" || raw === null) {\n    return Result.err(new MalformedWebhook(\"body is not an object\"));\n  }\n  return Result.ok(raw as StripeEvent);\n});\n// (raw: unknown) => Result<StripeEvent, MalformedWebhook>\n```\n\n> **It does not catch exceptions.** A `throw` inside the body still propagates, and an async body still rejects. That is deliberate: a throw is a bug, an error is an outcome. Use [`Result.try`](#try--run-it-now) when you want to convert one into the other — which is exactly what you do at the edge of an SDK that throws — or [`Result.fromThrowable`](#fromthrowable--convert-once-call-anywhere) to lift that SDK call once and stop thinking about it.\n\n---\n\n## Transforming\n\n### `mapValue` — the workhorse\n\nRuns on success, passes failures straight through. The callback returns a result, so it may introduce new errors, which are **added** to the union.\n\n```ts\nloadUser(userId) // AsyncResult<User, UserNotFound>\n  .mapValue((user) => requirePlan(user, \"pro\")) // + SubscriptionRequired\n  .mapValue((user) => loadWorkspace(user.orgId)) // + WorkspaceArchived\n  .mapValue((ws) => Result.ok(serialise(ws))); // no new failures\n// AsyncResult<WorkspaceDTO, UserNotFound | SubscriptionRequired | WorkspaceArchived>\n```\n\nA failure short-circuits the rest of the chain — later callbacks never run.\n\n### `mapError`\n\nRuns on failure, passes successes straight through. It sees the whole union at once, so the resulting error type is **replaced**, not narrowed:\n\n```ts\n// a public SDK method: collapse everything into one documented failure\nconst fetchInvoice = Result.registerExecution(async (id: string) =>\n  loadInvoice(id)\n    .mapError((e) => {\n      logger.warn({ tag: e._tag }, \"invoice lookup failed\");\n      return Result.err(new InvoiceUnavailable(id));\n    })\n    .getResult()\n);\n// AsyncResult<Invoice, InvoiceUnavailable>\n```\n\nTo deal with specific error types and leave the rest alone, use `handleError`.\n\n### `tap` / `tapError`\n\nSide effects that do not change the value — logging, metrics, tracing. If the effect returns a promise, the chain turns asynchronous and **waits for it**.\n\n```ts\nplaceOrder(cart)\n  .tap((order) => metrics.increment(\"orders.placed\", { plan: order.plan }))\n  .tapError((e) =>\n    logger.warn({ tag: e._tag, cartId: cart.id }, \"checkout failed\")\n  )\n  .tap(async (order) => await audit.record(\"order.created\", order.id)); // awaited\n```\n\n---\n\n## Handling errors\n\n`handleError` takes one or more error **classes** followed by a handler, and removes exactly those classes from the union:\n\n```ts\n//  AsyncResult<Dashboard, UserNotFound | SubscriptionRequired | DatabaseUnavailable>\nconst dashboard = loadDashboard(userId)\n  .handleError(UserNotFound, () => Result.ok(emptyDashboard))\n  //  AsyncResult<Dashboard, SubscriptionRequired | DatabaseUnavailable>\n  .handleError(SubscriptionRequired, (e) =>\n    Result.ok(upsellDashboard(e.requiredPlan))\n  )\n  //  AsyncResult<Dashboard, DatabaseUnavailable>\n  .handleError(DatabaseUnavailable, () =>\n    Result.ok(staleDashboardFromCache(userId))\n  );\n//  AsyncResult<Dashboard, never>   ← nothing left to handle\n\nconst view = (await dashboard.getResult()).unwrap(); // safe, and the compiler knows it\n```\n\nThe handler's parameter is narrowed to the classes you listed, so `e` is fully typed — including any data the error carries:\n\n```ts\ncallExternalApi(request)\n  .handleError(RateLimited, async (e) => {\n    // e.retryAfterSeconds: number\n    await sleep(e.retryAfterSeconds * 1000);\n    return callExternalApi(request).getResult();\n  })\n  .handleError(PaymentDeclined, CardExpired, (e) => {\n    // e: PaymentDeclined | CardExpired\n    return Result.err(new CheckoutFailed(e._tag));\n  });\n```\n\nA handler may also convert one error into another. The new error lands back in the union:\n\n```ts\nloadRow(id).handleError(DatabaseUnavailable, (e) =>\n  Result.err(new ServiceDegraded(e))\n);\n// AsyncResult<Row, RowNotFound | ServiceDegraded>\n```\n\nFour properties worth knowing:\n\n- **Matching is** `instanceof`, so handling a class also handles its subclasses.\n- **A handler never sees its own output.** Converting `A` into `B` and then handling `B` later in the chain works exactly as written; there is no re-entry.\n- **The handler runs at most once**, even if several of the listed classes match the same instance.\n- **Every class you name must still be in the union.** Handling something the `Result` cannot be carrying is a compile error, not a step that silently never runs:\n\n```ts\nconst dashboard = loadDashboard(userId).handleError(UserNotFound, () =>\n  Result.ok(emptyDashboard)\n);\n// AsyncResult<Dashboard, SubscriptionRequired | DatabaseUnavailable>\n\ndashboard.handleError(UserNotFound, handler); // ✗ already handled — not in the union\ndashboard.handleError(ParseError, handler); //   ✗ never was in the union\nResult.ok(1).handleError(UserNotFound, handler); // ✗ the union is `never`\n```\n\nA base class still names a union member that subclasses it, because matching is `instanceof` — `handleError(PaymentDeclined, …)` type-checks on a `Result` whose error is `CardExpired`. The one place the guard is stricter than the runtime is code that is generic over the error type: inside `<E>(r: Result<T, E>) => …` the compiler cannot see what `E` contains, so name a concrete union in the signature instead.\n\n---\n\n## Asynchronous chains\n\nAn asynchronous chain has a deliberately small surface: the five chaining methods, plus `getResult()`. Nothing reads a value — there is nothing to read until the chain settles.\n\n| Member                                                                 | on `Result` | on an async chain                 |\n| ---------------------------------------------------------------------- | ----------- | --------------------------------- |\n| `mapValue`, `mapError`, `handleError`, `tap`, `tapError`               | yes         | yes — identical signature         |\n| `getResult()`                                                          | yes         | yes — identical signature         |\n| `match()`, `unwrap()`, `unwrapError()`, `unwrapOr()`, `unwrapOrElse()` | yes         | **no** — call `getResult()` first |\n| `value`, `error`, `isOk`, `isErr`                                      | yes         | **no** — nothing to read yet      |\n| `toAsync()`                                                            | yes         | **no** — already one              |\n\n> Read the table as a **subset**: every member an asynchronous chain exposes also exists on `Result`, with the same signature. That is not a coincidence — it is what makes a [short-circuited step](#skipped-steps) safe, and it is asserted by a test.\n\nA chain is **not** a thenable. Finish it with `getResult()`, or with a terminal — those resolve on their own.\n\n```ts\nconst result = await loadUser(userId).getResult(); // Result<User, UserNotFound>\n\n// the terminals live on the Result, so resolve first\nconst user = result.unwrapOr(guestUser);\nconst status = result.match({\n  ok: () => 200 as const,\n  err: () => 404 as const,\n});\n\nawait loadUser(userId); // ✗ not thenable — hands back the chain\nawait loadUser(userId).unwrapOr(x); // ✗ a chain has no terminals\n```\n\n> Being a thenable would mean an `async` function returning a chain silently unwraps it, and a chain sitting in `Promise.all` resolves to something other than what you wrote. Keeping it a plain object makes `getResult()` the single, visible boundary between the chain and the promise world.\n\nA whole pipeline stays flat — you never `await` in the middle of it:\n\n```ts\nconst checkout = Result.registerExecution(async (cartId: string) =>\n  loadCart(cartId)\n    .mapValue((cart) => requireNonEmpty(cart)) // sync step\n    .mapValue(async (cart) => reserveInventory(cart)) // async step\n    .tap(async (cart) => await audit.record(\"inventory.reserved\", cart.id))\n    .mapValue((cart) => chargeSubscription(cart.userId, cart.totalCents))\n    .handleError(RateLimited, async (e) => {\n      await sleep(e.retryAfterSeconds * 1000);\n      return Result.err(new CheckoutBusy());\n    })\n    .getResult()\n);\n```\n\n`toAsync()` is the explicit lift, for when you want a chain to be asynchronous before any callback has made it so:\n\n```ts\nconst resolveTenant = (req: { headers: Record<string, string | undefined> }) =>\n  req.headers[\"x-tenant\"]\n    ? lookupTenant(String(req.headers[\"x-tenant\"])) // already a chain\n    : Result.err(new TenantMissing()).toAsync(); // lifted, so both branches match\n```\n\n> If the underlying promise **rejects** (something threw), the chain rejects too — it does not turn the rejection into an error branch. Wrap the throwing part in `Result.try` if you want that.\n\n---\n\n## Combining results\n\n`Result.all` turns a tuple of results into a result of a tuple, failing with the **first error in argument order**. It accepts synchronous results, asynchronous chains, and bare promises of results, in any mix:\n\n```ts\nconst page = await Result.all([\n  loadUser(userId), // an async chain\n  loadSubscription(userId), // an async chain\n  loadRecentOrders(userId), // an async chain\n  parseViewOptions(req.query), // a plain Result — no I/O\n]).getResult();\n// Result<\n//   [User, Subscription, Order[], ViewOptions],\n//   UserNotFound | SubscriptionMissing | DatabaseUnavailable | ValidationFailed\n// >\n\nconst view = page.mapValue(([user, sub, orders, opts]) =>\n  Result.ok(renderDashboard(user, sub, orders, opts))\n);\n```\n\nIf every member is synchronous you get a `Result` straight back, with no promise involved. If any member is asynchronous the whole call is, and every member runs **concurrently**.\n\nOrder is deterministic: the reported error is the first one in argument order, not the first to settle in time.\n\n`all` stops at the first failure. To keep every failure, use `[collect](#collecting-every-error)`.\n\n---\n\n## Collecting every error\n\n`Result.all` tells you _that_ something failed. `Result.collect` tells you **which** things failed. Same input, same tuple of values on success; on failure the error is a tuple the same length as the input, holding each member's error at its own index and `null` where that member succeeded.\n\n```ts\nconst form = Result.collect([\n  validateEmail(body.email), // Result<string, ValidationFailed>\n  validatePassword(body.password), // Result<string, ValidationFailed>\n  validateAge(body.age), // Result<number, ValidationFailed>\n]);\n// Result<\n//   [string, string, number],\n//   [ValidationFailed | null, ValidationFailed | null, ValidationFailed | null]\n// >\n\nconst response = form.match({\n  ok: ([email, password, age]) => ({\n    status: 200,\n    body: { email, password, age },\n  }),\n  err: (errors) => ({\n    status: 422,\n    body: {\n      fields: errors.flatMap((e) =>\n        e ? [{ field: e.field, reason: e.reason }] : []\n      ),\n    },\n  }),\n});\n// → 422 { fields: [{ field: \"password\", reason: \"too short\" },\n//                  { field: \"age\", reason: \"must be 18 or older\" }] }\n```\n\nThe index is the point: you know _which_ field failed, not merely that one did.\n\n|                          | `all`                    | `collect`                          |\n| ------------------------ | ------------------------ | ---------------------------------- | ----- |\n| Value on success         | tuple of values          | tuple of values (identical)        |\n| Error on failure         | the first error          | a tuple, `null` where it succeeded |\n| Error type               | a union of tagged errors | a tuple of `error                  | null` |\n| Works with `handleError` | yes                      | **no** — the error is a tuple      |\n| Reach for it when        | any failure means stop   | you must report every failure      |\n\n> Because the error is a tuple rather than a tagged error, `handleError()` cannot match on it — no error class is in that union, so the call does not compile. Read a collected failure with `match()` or `error`.\n\n```ts\nconst form = await Result.collect([\n  validateEmailFormat(body.email), // sync\n  ensureEmailIsFree(body.email), // async: one query\n  ensureUsernameIsFree(body.handle), // async: one query, runs alongside\n]).getResult();\n```\n\nIt follows the same dispatch rule as everything else: synchronous members give a `Result` straight back, and any asynchronous member makes the whole call asynchronous with the members running concurrently.\n\n---\n\n## Getting the value out\n\nThe terminals live on `Result` only. An asynchronous chain has none — you call `getResult()` first, and use them on the `Result` that comes back. That is deliberate: it is what makes a [short-circuited step](#skipped-steps) safe.\n\n```ts\nconst settled = await loadUser(userId).getResult();\nsettled.match({ ok: (u) => u.name, err: (e) => e._tag }); // and every other terminal\n```\n\n| Method                   | Returns                 | On the other branch            |\n| ------------------------ | ----------------------- | ------------------------------ | ----------------------------- | ----------- |\n| `match({ ok, err })`     | `A                      | B`                             | runs the other branch         |\n| `unwrap()`               | `T`                     | **throws** `ResultUnwrapError` |\n| `unwrapError()`          | `E`                     | **throws** `ResultUnwrapError` |\n| `unwrapOr(fallback)`     | `T                      | D`                             | returns `fallback`            |\n| `unwrapOrElse((e) => …)` | `T                      | D`                             | returns the computed fallback |\n| `getResult()`            | `Promise<Result<T, E>>` | —                              |\n| `isOk` / `isErr`         | `boolean`               | —                              |\n| `value` / `error`        | `T                      | undefined`/`E                  | undefined`                    | `undefined` |\n\n`match` is the exhaustive one — you cannot forget a branch:\n\n```ts\n// handleError collapses the union to the one failure this boundary reports…\nconst charge = await chargeSubscription(userId, 4900)\n  .handleError(UserNotFound, () =>\n    Result.err(new CheckoutFailed(404, \"no such user\"))\n  )\n  .handleError(NoPaymentMethod, () =>\n    Result.err(new CheckoutFailed(402, \"add a card first\"))\n  )\n  .handleError(PaymentDeclined, (e) =>\n    Result.err(new CheckoutFailed(402, `declined: ${e.failureCode}`))\n  )\n  .getResult(); // Result<Charge, CheckoutFailed>\n\n// …and match reads both branches, with nothing left to dispatch on.\nconst response = charge.match({\n  ok: (charge) => ({ status: 200, body: { receiptUrl: charge.receiptUrl } }),\n  err: (e) => ({ status: e.status, body: { error: e.reason } }),\n});\n```\n\n`unwrap()` is the only place this library throws on purpose. Once the union has been narrowed to `never` it is provably safe, which makes it the natural end of a fully-handled chain — and a reasonable thing to do at boot, where a failure should stop the process anyway.\n\n```ts\n// application startup: if the config is wrong, do not start.\n// loadConfig reads process.env — no I/O, so this whole chain is synchronous.\nconst config = loadConfig(process.env)\n  .handleError(MissingEnvVar, (e) =>\n    Result.err(new FatalMisconfiguration(e.name))\n  )\n  .unwrapOrElse((e) => {\n    logger.fatal({ tag: e._tag }, \"invalid configuration\");\n    process.exit(1);\n  });\n```\n\nReaching for it mid-chain throws away the guarantee you adopted the library for.\n\nThe thrown `ResultUnwrapError` carries the original failure on `.taggedError`, so nothing is lost:\n\n```ts\ntry {\n  (await loadUser(\"u_missing\").getResult()).unwrap();\n} catch (thrown) {\n  if (thrown instanceof ResultUnwrapError) {\n    thrown.taggedError; // the UserNotFound instance, with its userId\n    thrown.message; // 'Called unwrap() on an error Result (UserNotFound)'\n  }\n}\n```\n\n`value` and `error` are typed as `T | undefined` / `E | undefined` because a getter cannot narrow `this`. If you want the compiler to prove which branch you are on, use `match`.\n\n---\n\n## Bridging code that throws\n\nThis is the boundary between the throwing world — every SDK you did not write — and the typed one. Two entry points cross it: `try` runs the throwing call now, `fromThrowable` hands you a function that does.\n\n### try — run it now\n\n`Result.try` runs a function and converts anything it throws into a tagged error — and, like everything else, it follows the dispatch rule:\n\n```ts\n// a third-party SDK that rejects on network and HTTP errors alike\nconst charge = await Result.try(\n  () => stripe.charges.create({ amount, customer }),\n  (thrown) => new PaymentGatewayError(String(thrown))\n).getResult();\n// Result<Charge, PaymentGatewayError>\n\n// parsing a webhook body — synchronous, so a plain Result comes back\nconst event = Result.try(\n  () => JSON.parse(rawBody) as StripeEvent,\n  (thrown) => new MalformedWebhook(String(thrown))\n);\n// Result<StripeEvent, MalformedWebhook>\n```\n\nOn the asynchronous path it catches both a synchronous throw and a rejected promise.\n\n`onThrow` receives the thrown value as `unknown` — JavaScript does not guarantee it is an `Error`, and pretending otherwise is how `e.message` becomes `undefined` in production.\n\n### fromThrowable — convert once, call anywhere\n\n`Result.fromThrowable` is the same conversion, lifted: it **wraps** the function instead of running it. The throwing call and the failure it becomes are named one time, at the boundary; every call site afterwards is ordinary. Arguments and the dispatch rule pass straight through.\n\n```ts\nconst parseJson = Result.fromThrowable(\n  (raw: string) => JSON.parse(raw) as StripeEvent,\n  (thrown) => new MalformedWebhook(String(thrown))\n);\n// (raw: string) => Result<StripeEvent, MalformedWebhook>\n\nparseJson(rawBody).mapValue((event) => handle(event)); // no try in sight\n\nconst charge = Result.fromThrowable(\n  (amount: number, customer: string) =>\n    stripe.charges.create({ amount, customer }),\n  (thrown) => new PaymentGatewayError(String(thrown))\n);\n// (amount: number, customer: string) => AsyncResult<Charge, PaymentGatewayError>\n```\n\n> **Annotate what the throwing call returns.** Handing over a function typed `any` — `Result.fromThrowable(JSON.parse, …)` — leaves the compiler no way to tell a promise from a value, so it errs towards the asynchronous type. That is the safe direction, but not the one you want for `JSON.parse`: wrap it in an arrow with a return type, as above.\n\nThe mirror direction — going back to exceptions at the edge of your typed core — is `unwrap()`, or an explicit `throw` inside `match`.\n\n---\n\n## Recipes\n\n### An HTTP handler\n\n```ts\napp.get(\"/api/orders/:id\", async (req, res) => {\n  const settled = await loadOrder(req.params.id, req.user.id)\n    .mapValue((order) => Result.ok({ status: 200, headers: {}, body: order }))\n    .handleError(OrderNotFound, () =>\n      Result.ok({ status: 404, headers: {}, body: { error: \"not found\" } })\n    )\n    .handleError(NotYourOrder, () =>\n      Result.ok({ status: 403, headers: {}, body: { error: \"forbidden\" } })\n    )\n    .handleError(RateLimited, (e) =>\n      Result.ok({\n        status: 429,\n        headers: { \"Retry-After\": String(e.retryAfterSeconds) },\n        body: { error: \"slow down\" },\n      })\n    )\n    .handleError(DatabaseUnavailable, (e) => {\n      logger.error({ err: e }, \"order lookup failed\"); // a real Error: has a stack\n      return Result.ok({\n        status: 503,\n        headers: {},\n        body: { error: \"try again shortly\" },\n      });\n    })\n    .getResult(); // Result<Response, never>\n\n  const response = settled.unwrap(); // safe: nothing is left in the union\n  res.status(response.status).set(response.headers).json(response.body);\n});\n```\n\nEach handler receives exactly the classes it named — `e.retryAfterSeconds` is available only where it exists — and the union arriving at `never` is the proof that every failure got a status code.\n\n### Exhaustiveness at the boundary\n\nPin the handled chain to `never`. A chain that still carries a failure is not assignable to it, so a fourth failure upstream breaks this line rather than production:\n\n```ts\nconst respond = (\n  loaded: Result<Order, OrderNotFound | NotYourOrder | RateLimited>\n) => {\n  const handled: Result<HttpResponse, never> = loaded\n    .mapValue((order) => Result.ok(ok200(order)))\n    .handleError(OrderNotFound, () => Result.ok(status(404, \"not found\")))\n    .handleError(NotYourOrder, () => Result.ok(status(403, \"forbidden\")))\n    .handleError(RateLimited, (e) => Result.ok(retryAfter(e.retryAfterSeconds)));\n\n  return handled.unwrap();\n};\n```\n\n### Fallback chain\n\n```ts\nconst avatar = (\n  await fromCache(userId)\n    .handleError(CacheMiss, () => fromDatabase(userId))\n    .handleError(NotStored, () => fromGravatar(userId))\n    .tapError((e) => metrics.increment(\"avatar.miss\", { tag: e._tag }))\n    .getResult()\n).unwrapOr(defaultAvatarUrl);\n```\n\n### Keeping layers honest\n\nEach layer handles what it can and re-tags what it cannot, so the error union at the top is a list of things the caller must actually decide about:\n\n```ts\n// repository — infrastructure vocabulary only\nconst findOrderRow = Result.registerExecution(async (id: string) => { ... });\n// AsyncResult<OrderRow, RowNotFound | DatabaseUnavailable>\n\n// service — retires infrastructure detail, adds domain meaning\nconst loadOrder = Result.registerExecution(async (id: string, viewerId: string) =>\n  findOrderRow(id)\n    .handleError(RowNotFound, () => Result.err(new OrderNotFound(id)))\n    .mapValue((row) =>\n      row.userId === viewerId ? Result.ok(row) : Result.err(new NotYourOrder()),\n    )\n    .mapValue((row) => Result.ok(toDomain(row)))\n    .getResult(),\n);\n// AsyncResult<Order, OrderNotFound | NotYourOrder | DatabaseUnavailable | InvalidRow>\n\n// transport — the union above is the exact list of cases the handler must map\n```\n\n---\n\n## TypeScript notes\n\n### How the union moves\n\n| Operation                      | Effect on the error union                    |\n| ------------------------------ | -------------------------------------------- |\n| `mapValue(fn)`                 | adds whatever `fn` can fail with             |\n| `mapError(fn)`                 | **replaces** the union entirely              |\n| `handleError(A, B, fn)`        | removes `A` and `B`, adds `fn`'s errors      |\n| `tap` / `tapError`             | unchanged                                    |\n| `Result.all([...])`            | the union of every member                    |\n| `Result.registerExecution(fn)` | collapses a union of results into one result |\n\n### Naming an asynchronous chain\n\nThe asynchronous class is not exported, so you never write it by hand. When you do need to name one, infer it from a function that produces one:\n\n```ts\nconst loadOrder = Result.registerExecution(async (id: string) => { ... });\n\ntype OrderChain = ReturnType<typeof loadOrder>;\ntype OrderResult = Awaited<ReturnType<OrderChain[\"getResult\"]>>; // Result<Order, …>\n\n// middleware that works on any chain this function returns\nconst instrumented = (chain: OrderChain, route: string) =>\n  chain\n    .tap(() => metrics.increment(\"order.loaded\", { route }))\n    .tapError((e) => metrics.increment(\"order.failed\", { route, tag: e._tag }));\n```\n\nIn practice this comes up rarely: the chain is usually built and consumed in one expression, and `await` hands you back an ordinary `Result`, which _is_ exported and nameable.\n\n---\n\n## API reference\n\n### `Result` — statics\n\n| Signature                         | Description                                                                                               |\n| --------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| `ok(): Result<void, never>`       | Success carrying nothing.                                                                                 |\n| `ok<T>(value): Result<T, never>`  | Success carrying `value`.                                                                                 |\n| `empty(): Result<null, never>`    | Success carrying `null`.                                                                                  |\n| `err<E>(error): Result<never, E>` | Failure carrying a tagged error.                                                                          |\n| `try(fn, onThrow)`                | Runs `fn`, converting a throw — or a rejection — into a tagged error. Async when `fn` returns a promise.  |\n| `fromThrowable(fn, onThrow)`      | The same conversion, wrapped instead of run: returns `(...args) => Result<T, E>`.                         |\n| `registerExecution(fn)`           | Collapses a union of results into a result of unions. Async when the body is.                             |\n| `all(results)`                    | Tuple of results → result of a tuple. First error wins. Async if any member is; members run concurrently. |\n| `collect(results)`                | Same values, but the error is a **tuple** of every member's error with `null` where it succeeded.         |\n\n### `Result` — instance\n\nThe chaining methods and terminals below exist on an asynchronous chain too, under the same names with the same meanings — the async one just returns promises. The accessors and `toAsync()` are synchronous-only; `getResult()` is chain-only. There is no `then`: a chain is not thenable.\n\n| Signature                          | Description                                                           |\n| ---------------------------------- | --------------------------------------------------------------------- | ------------- | ---------- |\n| `isOk` / `isErr`                   | `boolean`                                                             |\n| `value` / `error`                  | `T                                                                    | undefined`/`E | undefined` |\n| `mapValue(fn)`                     | Transform the value; failures pass through. Errors accumulate.        |\n| `mapError(fn)`                     | Transform the error; successes pass through. Errors are replaced.     |\n| `handleError(...classes, handler)` | Handle specific classes and every descendant; subtracts them from the union. |\n| `tap(fn)` / `tapError(fn)`         | Side effect; returns the chain unchanged. An async effect is awaited. |\n| `match({ ok, err })`               | Collapse both branches into one value.                                |\n| `unwrap()` / `unwrapError()`       | Extract, or throw `ResultUnwrapError`.                                |\n| `unwrapOr(d)` / `unwrapOrElse(fn)` | Extract with a fallback.                                              |\n| `toAsync()`                        | Lift a synchronous result into an asynchronous chain. `Result` only.  |\n| `getResult()`                      | Resolve an asynchronous chain to a plain `Result`. Chain only.        |\n\n### Error base classes\n\n| Signature                | Description                                                                                                          |\n| ------------------------ | -------------------------------------------------------------------------------------------------------------------- |\n| `Tagged(tag)`            | Abstract base stamping a literal `_tag`. Each call returns a distinct class.                                          |\n| `Tagged(tag, Parent)`    | The same, as a member of `Parent`'s family: `_tag` becomes `\"Parent.tag\"` and `handleError(Parent)` handles it.       |\n| `TaggedError(tag)`       | The same as `Tagged(tag)`, but instances are real `Error`s with `message`, `stack` and `name === tag`.                |\n| `TaggedError(tag, Parent)` | A member of an `Error` family; `name` follows the composed path. `Parent` must already be an `Error` family.        |\n| `ResultUnwrapError`      | Thrown by `unwrap()` / `unwrapError()`. Carries the original error on `.taggedError`.                                 |\n\n### Branding values\n\n| Signature                | Description                                                                                                          |\n| ------------------------ | -------------------------------------------------------------------------------------------------------------------- |\n| `Branded<T, Name>`       | The base type `T` with a compile-time-only label. A raw `T` is not assignable to it; it is assignable to `T`.          |\n| `brand(name)`            | A constructor for `Branded<string, name>`. Identity at runtime. Base type via `brand<\"tag\", T>(name)`.                 |\n| `brand(name, is)`        | The same, checked: the constructor throws `InvalidBrand`, plus `is()` and `safe()`. Base type inferred from `is`.       |\n| `BrandOf<typeof X>`      | The type a `brand()` factory produces, so the name is written once.                                                   |\n| `Brand<T, Name>`         | The shape of an unchecked factory: callable, plus `brandName`. Use it to type one you are passed.                     |\n| `CheckedBrand<T, Name>`  | The same with the predicate behind it: adds `is()` and `safe()`.                                                      |\n| `InvalidBrand`           | Tagged `Error` a checked brand reports. Carries `brandName` and the refused `value`.                                  |\n\nA tag may not contain `.` — the factory composes the path, and a hand-written dot would claim a lineage `instanceof` does not back.\n\n---\n\n## Comparison\n\n|                                  | this                 | neverthrow          | Effect               |\n| -------------------------------- | -------------------- | ------------------- | -------------------- |\n| Error union in the type          | yes                  | yes                 | yes                  |\n| Handle by error class            | **yes, subtractive** | manual              | yes, via tags        |\n| Sync and async                   | **one API**          | two types, two APIs | one API              |\n| Runtime to install               | none                 | none                | yes                  |\n| Generator syntax                 | no                   | no                  | yes                  |\n| Dependency injection             | no                   | no                  | yes                  |\n| Concurrency, retries, scheduling | no                   | no                  | yes                  |\n| Bundle size                      | **1.4 KB** min+gz    | comparable          | substantially larger |\n| Names to import                  | **6**, plus 4 types  | a dozen or so       | many                 |\n\nPick Effect when you want the whole platform. Pick this when you want typed errors and nothing else in the way.\n\n---\n\nMIT © Dmytro Mykhailiuk\n","readmeFilename":"README.md"}