{"id":48115044,"url":"https://github.com/joao-coimbra/failcraft","last_synced_at":"2026-04-04T16:14:45.872Z","repository":{"id":346606185,"uuid":"1190755565","full_name":"joao-coimbra/failcraft","owner":"joao-coimbra","description":"Functional error handling for TypeScript — Either-based error handling with full type inference, chainable transforms, and async support","archived":false,"fork":false,"pushed_at":"2026-04-02T01:38:08.000Z","size":132,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-02T05:26:58.042Z","etag":null,"topics":["bun","either-monad","error-handling","functional-programming","typescript"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/failcraft","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joao-coimbra.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-24T15:33:36.000Z","updated_at":"2026-04-02T01:38:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/joao-coimbra/failcraft","commit_stats":null,"previous_names":["joao-coimbra/failcraft"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/joao-coimbra/failcraft","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joao-coimbra%2Ffailcraft","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joao-coimbra%2Ffailcraft/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joao-coimbra%2Ffailcraft/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joao-coimbra%2Ffailcraft/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joao-coimbra","download_url":"https://codeload.github.com/joao-coimbra/failcraft/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joao-coimbra%2Ffailcraft/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31405699,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bun","either-monad","error-handling","functional-programming","typescript"],"created_at":"2026-04-04T16:14:45.760Z","updated_at":"2026-04-04T16:14:45.834Z","avatar_url":"https://github.com/joao-coimbra.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cbr /\u003e\n\n# Failcraft\n\n### Functional error handling for TypeScript.\n\n`Either`-based error handling with full type inference, chainable transforms, and async support — no exceptions needed.\n\n\u003cbr /\u003e\n\n[![npm version](https://img.shields.io/npm/v/failcraft?style=for-the-badge\u0026logo=npm\u0026color=CB3837\u0026logoColor=white)](https://www.npmjs.com/package/failcraft)\n[![CI](https://img.shields.io/github/actions/workflow/status/joao-coimbra/failcraft/run-ci.yml?style=for-the-badge\u0026logo=github\u0026label=CI\u0026logoColor=white)](https://github.com/joao-coimbra/failcraft/actions/workflows/run-ci.yml)\n[![license](https://img.shields.io/badge/license-MIT-22C55E?style=for-the-badge)](./LICENSE)\n[![typescript](https://img.shields.io/badge/TypeScript-5-3178C6?style=for-the-badge\u0026logo=typescript\u0026logoColor=white)](https://www.typescriptlang.org/)\n[![bun](https://img.shields.io/badge/Bun-ready-F9F1E1?style=for-the-badge\u0026logo=bun\u0026logoColor=black)](https://bun.sh)\n\n\u003cbr /\u003e\n\n[Install](#install) · [Either](#either) · [Chaining](#chaining) · [Async](#async) · [Maybe](#maybe) · [Result](#result) · [Try helpers](#try-helpers) · [Attempt](#attempt) · [API](#api)\n\n\u003cbr /\u003e\n\n\u003c/div\u003e\n\n---\n\n## Install\n\n```bash\nbun add failcraft\n# or\nnpm install failcraft\n```\n\n---\n\n## Either\n\nAn `Either\u003cL, R\u003e` holds either a `Left` (error) or a `Right` (success). Use `left()` and `right()` to construct them:\n\n```ts\nimport { left, right } from 'failcraft'\n\nfunction divide(a: number, b: number) {\n  if (b === 0) return left(\"division by zero\")\n  return right(a / b)\n}\n\nconst result = divide(10, 2) // Either\u003cstring, number\u003e\n\nif (result.isRight()) {\n  console.log(result.value) // 5\n}\n\n// Aliases for domain-friendly code\nresult.isSuccess() // same as isRight()\nresult.isError()   // same as isLeft()\n```\n\n\u003e **Tip:** Always annotate the return type of functions that return `Either` through multiple branches. Without it, TypeScript infers a union of asymmetric types (`Left\u003c\"empty\", never\u003e | Right\u003cnumber, never\u003e`) that breaks method calls.\n\u003e\n\u003e ```ts\n\u003e // ❌ infers Left\u003c\"empty\", never\u003e | Right\u003cnumber, never\u003e\n\u003e function parse(s: string) {\n\u003e   if (!s) return left(\"empty\")\n\u003e   return right(Number(s))\n\u003e }\n\u003e\n\u003e // ✅ explicit return type collapses the union\n\u003e function parse(s: string): Either\u003c\"empty\", number\u003e {\n\u003e   if (!s) return left(\"empty\")\n\u003e   return right(Number(s))\n\u003e }\n\u003e ```\n\n---\n\n## Chaining\n\nTransform and chain computations without breaking out of the happy path:\n\n```ts\nconst result = divide(10, 2)\n  .transform(n =\u003e n * 100)        // maps the right value\n  .andThen(n =\u003e divide(n, 4))     // chains another Either-returning fn\n  .orDefault(0)                   // unwraps or falls back\n\n// Pattern match exhaustively\ndivide(10, 0).match({\n  left: (err) =\u003e `Error: ${err}`,\n  right: (val) =\u003e `Result: ${val}`,\n})\n\n// Side-effect tap (returns this, keeps chain alive)\ndivide(10, 2)\n  .on({ right: (val) =\u003e console.log(\"got\", val) })\n  .transform(n =\u003e n * 2)\n```\n\n---\n\n## Async\n\nPass an async function to `transform()` or `andThen()` and the chain automatically becomes an `AsyncEither\u003cL, R\u003e`. The key rule: **`await` goes once at the end of the chain**, not on each step.\n\n```ts\nimport { right } from 'failcraft'\n\nconst name = await right(1)\n  .transform(async (n) =\u003e fetchUser(n))    // Either → AsyncEither\n  .andThen(async (user) =\u003e saveUser(user)) // still AsyncEither\n  .transform((user) =\u003e user.name)          // sync step, still AsyncEither\n  .orDefault(\"anonymous\")\n  // Promise\u003cstring\u003e → await once here → string ✅\n```\n\nThe Promise is kept inside `AsyncEither` during the whole chain. Every terminator — `orDefault`, `getOrThrow`, `match` — resolves it and returns `Promise\u003cT\u003e`, which you `await` at the end.\n\n### `from()` — entry point for `Promise\u003cEither\u003e`\n\nWhen you already have a `Promise\u003cEither\u003e` (e.g. from an async function or `tryAsync`) and want to keep chaining, use `from()` to wrap it into `AsyncEither`:\n\n```ts\nimport { from } from 'failcraft'\n\nasync function findUser(id: number): Promise\u003cEither\u003c\"not_found\", User\u003e\u003e { ... }\nasync function findProfile(id: number): Promise\u003cEither\u003c\"no_profile\", Profile\u003e\u003e { ... }\n\n// from() lets you chain without intermediate awaits\nconst name = await from(findUser(1))\n  .andThen(user =\u003e findProfile(user.id))   // Promise\u003cEither\u003e accepted directly\n  .transform(profile =\u003e profile.name)\n  .orDefault(\"anonymous\")\n```\n\n**When to use which pattern:**\n\n```ts\n// Long chain with multiple async steps → from() + single await at the end\nconst name = await from(findUser(1))\n  .andThen(u =\u003e findProfile(u.id))\n  .transform(p =\u003e p.name.toUpperCase())\n  .orDefault(\"anonymous\")\n\n// Single async source, rest is sync → await the source directly\nconst result = await findUser(1)          // Either\u003c\"not_found\", User\u003e\nresult.transform(u =\u003e u.name).orDefault(\"anonymous\")\n```\n\n---\n\n## Maybe\n\n`Maybe\u003cT\u003e` represents an optional value — `Just\u003cT\u003e` (present) or `Nothing` (absent). Unlike `null` checks, it's composable and chainable:\n\n```ts\nimport { maybe, just, nothing } from 'failcraft'\n\n// maybe() wraps any value — null/undefined become Nothing, everything else Just\n// Note: falsy values like 0 and \"\" become Just (only null/undefined → Nothing)\nconst name = maybe(user.nickname)  // Maybe\u003cstring\u003e\n\nname\n  .transform(s =\u003e s.toUpperCase())           // maps if Just, skips if Nothing\n  .filter(s =\u003e s.length \u003e 2)                // Nothing if predicate fails\n  .orDefault(\"ANONYMOUS\")                   // unwrap with fallback\n\n// Pattern match\nname.match({\n  just: (n) =\u003e `Hello, ${n}!`,\n  nothing: () =\u003e \"Hello, stranger!\",\n})\n\n// Convert to Either\nname.toEither(\"no nickname set\")  // Either\u003cstring, string\u003e\n```\n\n`transform()` and `andThen()` also accept async functions, returning `AsyncMaybe\u003cT\u003e`:\n\n```ts\nmaybe(userId)\n  .andThen(async (id) =\u003e fetchUser(id))   // Maybe → AsyncMaybe\n  .transform((user) =\u003e user.name)\n  .orDefault(\"unknown\")\n  // returns Promise\u003cstring\u003e\n```\n\n---\n\n## Result\n\n`Result\u003cT, E\u003e` is a semantic alias for `Either\u003cE, T\u003e` with success-first parameters and `ok()`/`err()` constructors — ideal when you want readable error handling without custom classes:\n\n```ts\nimport { ok, err, type Result } from 'failcraft'\n\nasync function findUser(id: number): Promise\u003cResult\u003cUser, \"not_found\"\u003e\u003e {\n  const user = await db.users.findOne({ id })\n  return user ? ok(user) : err(\"not_found\")\n}\n\nconst result = await findUser(42)\n\nresult.match({\n  right: (user) =\u003e `Found: ${user.name}`,\n  left: (e) =\u003e `Error: ${e}`,              // e is typed as \"not_found\"\n})\n```\n\nSince `Result\u003cT, E\u003e` is just `Either\u003cE, T\u003e`, the entire `Either` API is available — `transform`, `andThen`, `orDefault`, `match`, and async overloads all work without any additional imports.\n\n---\n\n## Try helpers\n\nWrap functions that may throw without writing try/catch yourself:\n\n```ts\nimport { trySync, tryAsync } from 'failcraft'\n\n// Synchronous\nconst parsed = trySync(() =\u003e JSON.parse(rawJson))\n// Either\u003cunknown, unknown\u003e\n\n// Async — tryAsync returns Promise\u003cEither\u003e, compatible with from()\nconst data = await tryAsync(() =\u003e fetch(\"/api\").then(r =\u003e r.json()))\n// Either\u003cunknown, unknown\u003e\n\ndata\n  .transform((d) =\u003e d.items)\n  .match({\n    left: (err) =\u003e console.error(err),\n    right: (items) =\u003e console.log(items),\n  })\n```\n\n---\n\n## Attempt\n\n`attempt()` is a unified try/catch wrapper that automatically detects whether the function is sync or async, and accepts an optional `mapError` to transform the caught value:\n\n```ts\nimport { attempt } from 'failcraft'\n\n// Sync — returns Either\u003cunknown, unknown\u003e\nconst parsed = attempt(() =\u003e JSON.parse(rawJson))\n\n// Async — returns AsyncEither\u003cunknown, Data\u003e\nconst data = await attempt(async () =\u003e fetch(\"/api/data\").then(r =\u003e r.json()))\n\n// With error mapping — narrow the left type\nconst user = await attempt(\n  () =\u003e db.users.findOne(id),\n  (err) =\u003e err instanceof DatabaseError ? err.code : \"UNKNOWN\"\n)\n// AsyncEither\u003cstring, User\u003e\n```\n\nUse `attempt()` when you want a single import that handles both sync and async throws with optional error shaping. Use `trySync`/`tryAsync` for simpler cases where you don't need error mapping.\n\n---\n\n## API\n\n### `left(value)` / `right(value)`\n\nConstructors that return `Left\u003cL, R\u003e` and `Right\u003cR, L\u003e` respectively. Both are subtypes of `Either\u003cL, R\u003e`, so the full `Either` API is always available.\n\n### `Either\u003cL, R\u003e`\n\n| Method | Description |\n|---|---|\n| `.isLeft()` / `.isRight()` | Narrow the type to `Left` or `Right` |\n| `.isError()` / `.isSuccess()` | Aliases for `.isLeft()` / `.isRight()` |\n| `.transform(fn)` | Map the right value; async `fn` returns `AsyncEither` |\n| `.andThen(fn)` | Chain an `Either`-returning fn; async `fn` returns `AsyncEither` |\n| `.orDefault(value)` | Unwrap right or return fallback |\n| `.getOrThrow()` | Unwrap right or throw the left value |\n| `.getOrThrowWith(fn)` | Unwrap right or throw `fn(leftValue)` |\n| `.toMaybe()` | Convert to `Maybe\u003cR\u003e` — right becomes `Just`, left becomes `Nothing` |\n| `.on(cases)` | Side-effect tap; returns `this` |\n| `.match(cases)` | Exhaustive pattern match; returns `T` |\n\n### `AsyncEither\u003cL, R\u003e`\n\nSame interface as `Either` but every method returns `AsyncEither` or `Promise`. Extra method:\n\n| Method | Description |\n|---|---|\n| `.toPromise()` | Returns the underlying `Promise\u003cEither\u003cL, R\u003e\u003e` |\n\n### `from(promise)`\n\nWraps a `Promise\u003cEither\u003cL, R\u003e\u003e` into a chainable `AsyncEither\u003cL, R\u003e`, or a `Promise\u003cMaybe\u003cT\u003e\u003e` into a chainable `AsyncMaybe\u003cT\u003e`. Use this as the entry point whenever you have a `Promise\u003cEither\u003e` or `Promise\u003cMaybe\u003e` from an `async` function and want to keep chaining without intermediate `await` calls. The `await` goes once at the very end on the terminator (`orDefault`, `getOrThrow`, `match`).\n\n### `Maybe\u003cT\u003e`\n\n| Method | Description |\n|---|---|\n| `.isJust()` / `.isNothing()` | Narrow the type |\n| `.transform(fn)` | Map the value; async `fn` returns `AsyncMaybe` |\n| `.andThen(fn)` | Chain a `Maybe`-returning fn; async `fn` returns `AsyncMaybe` |\n| `.filter(predicate)` | Return `Nothing` when predicate fails |\n| `.orDefault(value)` | Unwrap or return fallback |\n| `.orNothing()` | Unwrap to `T \\| undefined` |\n| `.orThrow(error)` | Unwrap or throw |\n| `.toEither(leftValue)` | Convert to `Either` — `Just` → `right`, `Nothing` → `left` |\n| `.on(cases)` | Side-effect tap; returns `this` |\n| `.match(cases)` | Exhaustive pattern match; returns `T` |\n\n### `maybe(value)` / `just(value)` / `nothing()`\n\n`just(value)` returns `Just\u003cT\u003e`, `nothing()` returns `Nothing`, and `maybe(value)` returns `Maybe\u003cNonNullable\u003cT\u003e\u003e` — mapping `null`/`undefined` to `Nothing`, everything else to `Just`.\n\n### `AsyncMaybe\u003cT\u003e`\n\nSame interface as `Maybe` but every method returns `AsyncMaybe` or `Promise`. Extra method:\n\n| Method | Description |\n|---|---|\n| `.toPromise()` | Returns the underlying `Promise\u003cMaybe\u003cT\u003e\u003e` |\n\n### `Result\u003cT, E\u003e`\n\nType alias: `Result\u003cT, E\u003e` ≡ `Either\u003cE, T\u003e`. Use with `ok(value)` / `err(error)` constructors.\n\n### `trySync(fn)` / `tryAsync(fn)`\n\nWrap a possibly-throwing function. `trySync` returns `Either`, `tryAsync` returns `Promise\u003cEither\u003e`.\n\n### `attempt(fn, mapError?)`\n\nUnified try/catch wrapper that auto-detects sync vs async from the function signature. Returns `Either\u003cL, R\u003e` for sync functions and `AsyncEither\u003cL, R\u003e` for async ones. The optional `mapError` transforms the caught `unknown` error into the left type `L`.\n\n---\n\n## Development\n\nRequirements: **Bun \u003e= 1.0**\n\n```bash\nbun install          # install dependencies\nbun test             # run unit tests\nbun x ultracite fix  # lint + format\n```\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\n**Built with ❤️ for the TypeScript community.**\n\n[Contributing](./CONTRIBUTING.md) · [Code of Conduct](./CODE_OF_CONDUCT.md) · [MIT License](./LICENSE)\n\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoao-coimbra%2Ffailcraft","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoao-coimbra%2Ffailcraft","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoao-coimbra%2Ffailcraft/lists"}