[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-74967":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":22,"archived":23,"fork":23,"defaultBranch":24,"hasWiki":25,"hasPages":23,"topics":26,"createdAt":10,"pushedAt":10,"updatedAt":32,"readmeContent":33,"aiSummary":34,"trendingCount":16,"starSnapshotCount":16,"syncStatus":35,"lastSyncTime":36,"discoverSource":37},74967,"better-result","dmmulroy\u002Fbetter-result","dmmulroy","Lightweight Result type for TypeScript with generator-based composition.","",null,"TypeScript",1524,36,1,8,0,7,29,72,21,17.7,"MIT License",false,"main",true,[27,28,29,30,31],"error-handling","result-type","rust","try-catch","typescript","2026-06-12 02:03:30","# better-result\n\nLightweight Result type for TypeScript with generator-based composition.\n\n📖 **[Documentation](https:\u002F\u002Fbetter-result.dev\u002Fcore\u002Fcreating-results)**\n\n## Install\n\n```sh\nnpm install better-result\n```\n\nOr with Bun \u002F pnpm:\n\n```sh\nbun add better-result\npnpm add better-result\n```\n\n## Quick Start\n\n```ts\nimport { Result } from \"better-result\";\n\n\u002F\u002F Wrap throwing functions\nconst parsed = Result.try(() => JSON.parse(input));\n\n\u002F\u002F Check and use\nif (Result.isOk(parsed)) {\n  console.log(parsed.value);\n} else {\n  console.error(parsed.error);\n}\n\n\u002F\u002F Or use pattern matching\nconst message = parsed.match({\n  ok: (data) => `Got: ${data.name}`,\n  err: (e) => `Failed: ${e.message}`,\n});\n```\n\n## Contents\n\n- [Creating Results](#creating-results)\n- [Transforming Results](#transforming-results)\n- [Handling Errors](#handling-errors)\n- [Observing Results](#observing-results)\n- [Extracting Values](#extracting-values)\n- [Generator Composition](#generator-composition)\n- [Retry Support](#retry-support)\n- [UnhandledException](#unhandledexception)\n- [Panic](#panic)\n- [Tagged Errors](#tagged-errors)\n- [Serialization](#serialization)\n- [API Reference](#api-reference)\n- [Agents & AI](#agents--ai)\n\n## Creating Results\n\n```ts\n\u002F\u002F Success\nconst ok = Result.ok(42);\n\n\u002F\u002F Error\nconst err = Result.err(new Error(\"failed\"));\n\n\u002F\u002F From throwing function\nconst result = Result.try(() => riskyOperation());\n\n\u002F\u002F From promise\nconst result = await Result.tryPromise(() => fetch(url));\n\n\u002F\u002F With custom error handling\nconst result = Result.try({\n  try: () => JSON.parse(input),\n  catch: (e) => new ParseError(e),\n});\n```\n\n## Transforming Results\n\n```ts\nconst result = Result.ok(2)\n  .map((x) => x * 2) \u002F\u002F Ok(4)\n  .andThen(\n    (\n      x, \u002F\u002F Chain Result-returning functions\n    ) => (x > 0 ? Result.ok(x) : Result.err(\"negative\")),\n  );\n\n\u002F\u002F Standalone functions (data-first or data-last)\nResult.map(result, (x) => x + 1);\nResult.map((x) => x + 1)(result); \u002F\u002F Pipeable\n```\n\n## Handling Errors\n\n```ts\n\u002F\u002F Transform error type\nconst result = fetchUser(id).mapError((e) => new AppError(`Failed to fetch user: ${e.message}`));\n\n\u002F\u002F Recover from specific errors while preserving the same success type\nconst result = fetchUser(id).tryRecover((e) =>\n  e._tag === \"NotFoundError\" ? Result.ok(defaultUser) : Result.err(e),\n);\n\n\u002F\u002F Async recovery follows the same pattern\n\u002F\u002F If fetchUser is async and returns Promise\u003CResult\u003CUser, E>>, await it first.\nconst result = await (\n  await fetchUser(id)\n).tryRecoverAsync(async (e) =>\n  e._tag === \"NetworkError\" ? Result.ok(await readUserFromCache(id)) : Result.err(e),\n);\n```\n\n## Observing Results\n\nUse `tap` \u002F `tapAsync` for success-side logging or tracing, `tapError` \u002F `tapErrorAsync` for error-side logging or tracing, and `tapBoth` \u002F `tapBothAsync` when you want to observe either branch with one handler object. These methods do not transform the `Result` — they always return the original value unchanged.\n\n```ts\nconst result = Result.try(() => JSON.parse(input))\n  .tap((value) => {\n    console.debug(\"parsed payload\", value);\n  })\n  .tapError((error) => {\n    console.error(\"failed to parse payload\", error);\n  });\n```\n\nIf you want to observe both branches symmetrically with one call, use `tapBoth`:\n\n```ts\nconst result = Result.try(() => JSON.parse(input)).tapBoth({\n  ok: (value) => {\n    console.info(\"decoded payload\", value);\n  },\n  err: (error) => {\n    console.warn(\"decode failed\", error);\n  },\n});\n```\n\nAsync side effects follow the same pattern:\n\n```ts\nconst result = await Result.err(\"request failed\").tapErrorAsync(async (error) => {\n  await trace(\"request.failed\", { error });\n});\n```\n\n`tapBothAsync` works the same way for async observers on either branch:\n\n```ts\nconst observed = await Result.tapBothAsync(\n  Result.try(() => JSON.parse(input)),\n  {\n    ok: async (value) => {\n      await trace(\"payload.decoded\", { value });\n    },\n    err: async (error) => {\n      await trace(\"payload.decode_failed\", { error });\n    },\n  },\n);\n```\n\nStatic helpers support both data-first and data-last styles:\n\n```ts\nconst traced = Result.tapError(Result.err(\"cache miss\"), (error) => {\n  console.warn(\"cache lookup failed\", error);\n});\n\nconst traceError = Result.tapErrorAsync(async (error: string) => {\n  await trace(\"cache.lookup_failed\", { error });\n});\n\nawait traceError(Result.err(\"cache miss\"));\n```\n\nIf you prefer, you can still observe both branches by chaining `tap` and `tapError` separately.\n\nThrown or rejected side-effect callbacks become `Panic`, just like other Result callbacks.\n\n## Extracting Values\n\n```ts\n\u002F\u002F Unwrap (throws on Err)\nconst value = result.unwrap();\nconst value = result.unwrap(\"custom error message\");\n\n\u002F\u002F With fallback\nconst value = result.unwrapOr(defaultValue);\n\n\u002F\u002F Pattern match\nconst value = result.match({\n  ok: (v) => v,\n  err: (e) => fallback,\n});\n```\n\n## Generator Composition\n\nChain multiple Results without nested callbacks or early returns:\n\n```ts\nconst result = Result.gen(function* () {\n  const a = yield* parseNumber(inputA); \u002F\u002F Unwraps or short-circuits\n  const b = yield* parseNumber(inputB);\n  const c = yield* divide(a, b);\n  return Result.ok(c);\n});\n\u002F\u002F Result\u003Cnumber, ParseError | DivisionError>\n```\n\nAsync version with `Result.await`:\n\n```ts\nconst result = await Result.gen(async function* () {\n  const user = yield* Result.await(fetchUser(id));\n  const posts = yield* Result.await(fetchPosts(user.id));\n  return Result.ok({ user, posts });\n});\n```\n\nErrors from all yielded Results are automatically collected into the final error union type.\n\n### Normalizing Error Types\n\nUse `mapError` on the output of `Result.gen()` to unify multiple error types into a single type:\n\n```ts\nclass ParseError extends TaggedError(\"ParseError\")\u003C{ message: string }>() {}\nclass ValidationError extends TaggedError(\"ValidationError\")\u003C{ message: string }>() {}\nclass AppError extends TaggedError(\"AppError\")\u003C{ source: string; message: string }>() {}\n\nconst result = Result.gen(function* () {\n  const parsed = yield* parseInput(input); \u002F\u002F Err: ParseError\n  const valid = yield* validate(parsed); \u002F\u002F Err: ValidationError\n  return Result.ok(valid);\n}).mapError((e): AppError => new AppError({ source: e._tag, message: e.message }));\n\u002F\u002F Result\u003CValidatedData, AppError> - error union normalized to single type\n```\n\n## Retry Support\n\n```ts\nconst result = await Result.tryPromise(() => fetch(url), {\n  retry: {\n    times: 3,\n    delayMs: 100,\n    backoff: \"exponential\", \u002F\u002F or \"linear\" | \"constant\"\n  },\n});\n```\n\n### Conditional Retry\n\nRetry only for specific error types using `shouldRetry`:\n\n```ts\nclass NetworkError extends TaggedError(\"NetworkError\")\u003C{ message: string }>() {}\nclass ValidationError extends TaggedError(\"ValidationError\")\u003C{ message: string }>() {}\n\nconst result = await Result.tryPromise(\n  {\n    try: () => fetchData(url),\n    catch: (e) =>\n      e instanceof TypeError \u002F\u002F Network failures often throw TypeError\n        ? new NetworkError({ message: (e as Error).message })\n        : new ValidationError({ message: String(e) }),\n  },\n  {\n    retry: {\n      times: 3,\n      delayMs: 100,\n      backoff: \"exponential\",\n      shouldRetry: (e) => e._tag === \"NetworkError\", \u002F\u002F Only retry network errors\n    },\n  },\n);\n```\n\n### Async Retry Decisions\n\nFor retry decisions that require async operations (rate limits, feature flags, etc.), enrich the error in the `catch` handler instead of making `shouldRetry` async:\n\n```ts\nclass ApiError extends TaggedError(\"ApiError\")\u003C{\n  message: string;\n  rateLimited: boolean;\n}>() {}\n\nconst result = await Result.tryPromise(\n  {\n    try: () => callApi(url),\n    catch: async (e) => {\n      \u002F\u002F Fetch async state in catch handler\n      const retryAfter = await redis.get(`ratelimit:${userId}`);\n      return new ApiError({\n        message: (e as Error).message,\n        rateLimited: retryAfter !== null,\n      });\n    },\n  },\n  {\n    retry: {\n      times: 3,\n      delayMs: 100,\n      backoff: \"exponential\",\n      shouldRetry: (e) => !e.rateLimited, \u002F\u002F Sync predicate uses enriched error\n    },\n  },\n);\n```\n\n## UnhandledException\n\nWhen `Result.try()` or `Result.tryPromise()` catches an exception without a custom handler, the error type is `UnhandledException`:\n\n```ts\nimport { Result, UnhandledException } from \"better-result\";\n\n\u002F\u002F Automatic — error type is UnhandledException\nconst result = Result.try(() => JSON.parse(input));\n\u002F\u002F    ^? Result\u003Cunknown, UnhandledException>\n\n\u002F\u002F Custom handler — you control the error type\nconst result = Result.try({\n  try: () => JSON.parse(input),\n  catch: (e) => new ParseError(e),\n});\n\u002F\u002F    ^? Result\u003Cunknown, ParseError>\n\n\u002F\u002F Same for async\nawait Result.tryPromise(() => fetch(url));\n\u002F\u002F    ^? Promise\u003CResult\u003CResponse, UnhandledException>>\n```\n\nAccess the original exception via `.cause`:\n\n```ts\nif (Result.isError(result)) {\n  const original = result.error.cause;\n  if (original instanceof SyntaxError) {\n    \u002F\u002F Handle JSON parse error\n  }\n}\n```\n\n## Panic\n\nThrown (not returned) when user callbacks throw inside Result operations. Represents a defect in your code, not a domain error.\n\n```ts\nimport { Panic, isPanic } from \"better-result\";\n\n\u002F\u002F Callback throws → Panic\nResult.ok(1).map(() => {\n  throw new Error(\"bug\");\n}); \u002F\u002F throws Panic\n\n\u002F\u002F Generator cleanup throws → Panic\nResult.gen(function* () {\n  try {\n    yield* Result.err(\"expected failure\");\n  } finally {\n    throw new Error(\"cleanup bug\");\n  }\n}); \u002F\u002F throws Panic\n\n\u002F\u002F Catch handler throws → Panic\nResult.try({\n  try: () => riskyOp(),\n  catch: () => {\n    throw new Error(\"bug in handler\");\n  },\n}); \u002F\u002F throws Panic\n\n\u002F\u002F Catching Panic (for error reporting)\ntry {\n  result.map(() => {\n    throw new Error(\"bug\");\n  });\n} catch (error) {\n  if (isPanic(error)) {\n    \u002F\u002F isPanic() is a type guard function\n    console.error(\"Defect:\", error.message, error.cause);\n  }\n\n  if (Panic.is(error)) {\n    \u002F\u002F Panic.is() is a static method (same behavior)\n  }\n\n  if (error instanceof Panic) {\n    \u002F\u002F instanceof works too\n  }\n}\n```\n\n**Why Panic?** `Err` is for recoverable domain errors. Panic is for bugs — like Rust's `panic!()`. If your `.map()` callback throws, that's not an error to handle, it's a defect to fix. Returning `Err` would collapse type safety (`Result\u003CT, E>` becomes `Result\u003CT, E | unknown>`).\n\n**Panic properties:**\n\n| Property  | Type      | Description                   |\n| --------- | --------- | ----------------------------- |\n| `message` | `string`  | Describes where\u002Fwhat panicked |\n| `cause`   | `unknown` | The exception that was thrown |\n\nPanic also provides `toJSON()` for error reporting services (Sentry, etc.).\n\n## Tagged Errors\n\nBuild exhaustive error handling with discriminated unions:\n\n```ts\nimport { Result, TaggedError, matchError, matchErrorPartial } from \"better-result\";\n\n\u002F\u002F Factory API: TaggedError(\"Tag\")\u003CProps>()\nclass NotFoundError extends TaggedError(\"NotFoundError\")\u003C{\n  id: string;\n  message: string;\n}>() {}\n\nclass ValidationError extends TaggedError(\"ValidationError\")\u003C{\n  field: string;\n  message: string;\n}>() {}\n\ntype AppError = NotFoundError | ValidationError;\n\n\u002F\u002F Create errors with object args\nconst err = new NotFoundError({ id: \"123\", message: \"User not found\" });\n\n\u002F\u002F Exhaustive matching\nmatchError(error, {\n  NotFoundError: (e) => `Missing: ${e.id}`,\n  ValidationError: (e) => `Bad field: ${e.field}`,\n});\n\n\u002F\u002F Partial matching with fallback\nmatchErrorPartial(\n  error,\n  { NotFoundError: (e) => `Missing: ${e.id}` },\n  (e) => `Unknown: ${e.message}`,\n);\n\n\u002F\u002F Type guards\nTaggedError.is(value); \u002F\u002F any tagged error\nNotFoundError.is(value); \u002F\u002F specific class\n```\n\n### Yielding Tagged Errors in `Result.gen`\n\nTagged errors can short-circuit `Result.gen` directly. This is useful for recoverable domain errors and is equivalent to yielding `Result.err(error)`; it does not throw.\n\n```ts\nconst result = Result.gen(function* () {\n  yield* new NotFoundError({ id: \"123\", message: \"missing\" });\n  return Result.ok(\"never reached\");\n});\n\u002F\u002F Result\u003Cstring, NotFoundError>\n\u002F\u002F => Err(original NotFoundError instance)\n```\n\nThey also compose with regular `Result` values and contribute to the inferred error union:\n\n```ts\nconst result = Result.gen(function* () {\n  const user = yield* findUser(\"123\"); \u002F\u002F Result\u003CUser, NotFoundError>\n\n  if (!user.active) {\n    yield* new ValidationError({ field: \"active\", message: \"User is inactive\" });\n  }\n\n  return Result.ok(user);\n});\n\u002F\u002F Result\u003CUser, NotFoundError | ValidationError>\n```\n\nFor errors with computed messages, add a custom constructor:\n\n```ts\nclass NetworkError extends TaggedError(\"NetworkError\")\u003C{\n  url: string;\n  status: number;\n  message: string;\n}>() {\n  constructor(args: { url: string; status: number }) {\n    super({ ...args, message: `Request to ${args.url} failed: ${args.status}` });\n  }\n}\n\nnew NetworkError({ url: \"\u002Fapi\", status: 404 });\n```\n\n## Serialization\n\nConvert Results to plain objects for RPC, storage, or server actions:\n\n```ts\nimport { Result, SerializedResult, ResultDeserializationError } from \"better-result\";\n\n\u002F\u002F Serialize to plain object\nconst result = Result.ok(42);\nconst serialized = Result.serialize(result);\n\u002F\u002F { status: \"ok\", value: 42 }\n\n\u002F\u002F Deserialize back to Result instance\nconst deserialized = Result.deserialize\u003Cnumber, never>(serialized);\n\u002F\u002F Ok(42) - can use .map(), .andThen(), etc.\n\n\u002F\u002F Invalid input returns ResultDeserializationError\nconst invalid = Result.deserialize({ foo: \"bar\" });\nif (Result.isError(invalid) && ResultDeserializationError.is(invalid.error)) {\n  console.log(\"Bad input:\", invalid.error.value);\n}\n\n\u002F\u002F Typed boundary for Next.js server actions\nasync function createUser(data: FormData): Promise\u003CSerializedResult\u003CUser, ValidationError>> {\n  const result = await validateAndCreate(data);\n  return Result.serialize(result);\n}\n\n\u002F\u002F Client-side\nconst serialized = await createUser(formData);\nconst result = Result.deserialize\u003CUser, ValidationError>(serialized);\n```\n\n## API Reference\n\n### Result\n\n| Method                                  | Description                                                                              |\n| --------------------------------------- | ---------------------------------------------------------------------------------------- |\n| `Result.ok(value)`                      | Create success                                                                           |\n| `Result.err(error)`                     | Create error                                                                             |\n| `Result.try(fn)`                        | Wrap throwing function                                                                   |\n| `Result.tryPromise(fn, config?)`        | Wrap async function with optional retry                                                  |\n| `Result.isOk(result)`                   | Type guard for Ok                                                                        |\n| `Result.isError(result)`                | Type guard for Err                                                                       |\n| `Result.gen(fn)`                        | Generator composition                                                                    |\n| `Result.tryRecover(result, fn)`         | Recover error into same success type                                                     |\n| `Result.tryRecoverAsync(result, fn)`    | Async recover error into same success type                                               |\n| `Result.tap(result, fn)`                | Run side effect on success and return original result                                    |\n| `Result.tapAsync(result, fn)`           | Run async side effect on success and return original result                              |\n| `Result.tapError(result, fn)`           | Run side effect on error and return original result                                      |\n| `Result.tapErrorAsync(result, fn)`      | Run async side effect on error and return original result                                |\n| `Result.tapBoth(result, handlers)`      | Run side effect on either branch and return original result                              |\n| `Result.tapBothAsync(result, handlers)` | Run async side effect on either branch and return original result                        |\n| `Result.await(promise)`                 | Wrap Promise\u003CResult> for generators                                                      |\n| `Result.serialize(result)`              | Convert Result to plain object                                                           |\n| `Result.deserialize(value)`             | Rehydrate serialized Result (returns `Err\u003CResultDeserializationError>` on invalid input) |\n| `Result.partition(results)`             | Split array into [okValues, errValues]                                                   |\n| `Result.flatten(result)`                | Flatten nested Result                                                                    |\n\n### Instance Methods\n\n| Method                    | Description                                |\n| ------------------------- | ------------------------------------------ |\n| `.isOk()`                 | Type guard, narrows to Ok                  |\n| `.isErr()`                | Type guard, narrows to Err                 |\n| `.map(fn)`                | Transform success value                    |\n| `.mapError(fn)`           | Transform error value                      |\n| `.tryRecover(fn)`         | Recover error into same success type       |\n| `.tryRecoverAsync(fn)`    | Async recover error into same success type |\n| `.andThen(fn)`            | Chain Result-returning function            |\n| `.andThenAsync(fn)`       | Chain async Result-returning function      |\n| `.match({ ok, err })`     | Pattern match                              |\n| `.unwrap(message?)`       | Extract value or throw                     |\n| `.unwrapOr(fallback)`     | Extract value or return fallback           |\n| `.tap(fn)`                | Side effect on success                     |\n| `.tapAsync(fn)`           | Async side effect on success               |\n| `.tapError(fn)`           | Side effect on error                       |\n| `.tapErrorAsync(fn)`      | Async side effect on error                 |\n| `.tapBoth(handlers)`      | Side effect on either branch               |\n| `.tapBothAsync(handlers)` | Async side effect on either branch         |\n\n### TaggedError\n\n| Method                                 | Description                        |\n| -------------------------------------- | ---------------------------------- |\n| `TaggedError(tag)\u003CProps>()`            | Factory for tagged error class     |\n| `TaggedError.is(value)`                | Type guard for any TaggedError     |\n| `matchError(err, handlers)`            | Exhaustive pattern match by `_tag` |\n| `matchErrorPartial(err, handlers, fb)` | Partial match with fallback        |\n| `isTaggedError(value)`                 | Type guard (standalone function)   |\n| `panic(message, cause?)`               | Throw unrecoverable Panic          |\n| `isPanic(value)`                       | Type guard for Panic               |\n\n### Type Helpers\n\n| Type                     | Description                  |\n| ------------------------ | ---------------------------- |\n| `InferOk\u003CR>`             | Extract Ok type from Result  |\n| `InferErr\u003CR>`            | Extract Err type from Result |\n| `SerializedResult\u003CT, E>` | Plain object form of Result  |\n| `SerializedOk\u003CT>`        | Plain object form of Ok      |\n| `SerializedErr\u003CE>`       | Plain object form of Err     |\n\n## Agents & AI\n\nbetter-result ships with portable `SKILL.md` skills instead of an interactive CLI.\n\n### Available skills\n\n- `better-result-adopt` — adopt `better-result` in an existing codebase\n- `better-result-migrate-v2` — migrate v1 `TaggedError` usage to the v2 API\n\nThese skills are designed to work with SKILL.md-compatible agents and skills.sh-compatible tooling.\n\n### Install with skills.sh-compatible tooling\n\n```sh\nnpx skills add dmmulroy\u002Fbetter-result@better-result-adopt\nnpx skills add dmmulroy\u002Fbetter-result@better-result-migrate-v2\n```\n\nTo install globally without prompts:\n\n```sh\nnpx skills add dmmulroy\u002Fbetter-result@better-result-adopt -g -y\n```\n\n### Manual installation\n\nIf your agent does not support skills.sh installation, copy one of these directories into the agent's skills folder:\n\n- `skills\u002Fbetter-result-adopt\u002F`\n- `skills\u002Fbetter-result-migrate-v2\u002F`\n\n### What the skills do\n\n`better-result-adopt` guides an agent through:\n\n- converting try\u002Fcatch to `Result.try` \u002F `Result.tryPromise`\n- defining `TaggedError` classes for domain errors\n- refactoring nested error handling into `Result.gen`\n- replacing nullable or sentinel error returns with `Result`\n\n`better-result-migrate-v2` guides an agent through:\n\n- migrating `TaggedError` classes from v1 to v2 factory syntax\n- updating constructor call sites to the new object form\n- replacing `TaggedError.match*` helpers with standalone helpers\n- updating imports and verifying no old API usages remain\n\n### Optional source context\n\nFor richer AI context in a consuming project:\n\n```sh\nnpx opensrc better-result\n```\n\nSee [skills\u002FREADME.md](skills\u002FREADME.md) for a concise skill-install reference.\n\n## License\n\nMIT\n","better-result 是一个为 TypeScript 设计的轻量级 Result 类型库，支持基于生成器的组合。其核心功能包括创建、转换和处理结果类型，以及错误处理，提供了丰富的 API 用于操作结果对象，如 `map`、`andThen`、`tryRecover` 等，并且能够优雅地管理异步操作中的错误。该库特别适用于需要精细控制错误流程的应用场景，比如后端服务开发中对用户输入验证失败或外部API调用异常等情况下的错误处理。通过使用 better-result，开发者可以编写出更加健壮且易于维护的代码。",2,"2026-06-11 03:51:44","high_star"]