{"id":25983760,"url":"https://github.com/adamhl8/ts-explicit-errors","last_synced_at":"2025-03-05T10:32:30.287Z","repository":{"id":268470037,"uuid":"904461999","full_name":"adamhl8/ts-explicit-errors","owner":"adamhl8","description":"A concise and type-safe error handling library for TypeScript","archived":false,"fork":false,"pushed_at":"2025-02-28T22:17:01.000Z","size":288,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-28T23:53:57.169Z","etag":null,"topics":["error","error-handling","errors","result","result-type","typescript"],"latest_commit_sha":null,"homepage":"","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/adamhl8.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"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}},"created_at":"2024-12-16T23:57:32.000Z","updated_at":"2025-02-28T22:18:27.000Z","dependencies_parsed_at":"2024-12-17T00:58:46.108Z","dependency_job_id":"3171a394-ddb7-4571-be5a-fce8beaf71fb","html_url":"https://github.com/adamhl8/ts-explicit-errors","commit_stats":null,"previous_names":["adamhl8/ts-error-tuple","adamhl8/ts-explicit-errors"],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamhl8%2Fts-explicit-errors","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamhl8%2Fts-explicit-errors/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamhl8%2Fts-explicit-errors/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamhl8%2Fts-explicit-errors/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adamhl8","download_url":"https://codeload.github.com/adamhl8/ts-explicit-errors/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242010284,"owners_count":20057220,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["error","error-handling","errors","result","result-type","typescript"],"created_at":"2025-03-05T10:32:29.737Z","updated_at":"2025-03-05T10:32:30.248Z","avatar_url":"https://github.com/adamhl8.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ts-explicit-errors\n\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![npm version](https://img.shields.io/npm/v/ts-explicit-errors.svg)](https://www.npmjs.com/package/ts-explicit-errors)\n\nA concise and type-safe error handling library for TypeScript that provides explicit error handling with added support for error context.\n\n- Zero dependencies (the whole library is only ~70 LoC)\n- Small, easy to understand API\n- Attach and retrieve context data from errors\n\nThis allows you to treat errors as values so you can write more safe, readable, and maintainable code.\n\n---\n\n- [Installation](#installation)\n- [Usage](#usage)\n- [Rationale](#rationale)\n- [API](#api)\n  - [`Result` Type](#result-type)\n  - [`isErr` Function](#iserr-function)\n  - [`attempt` Function](#attempt-function)\n  - [`err` Function](#err-function)\n  - [`CtxError` Class](#ctxerror-class)\n    - [`ctx` Method](#ctx-method)\n    - [`get` Method](#get-method)\n    - [`getAll` Method](#getall-method)\n    - [`fmtErr` Method](#fmterr-method)\n  - [`errWithCtx` Function](#errwithctx-function)\n- [Example](#example)\n\n## Installation\n\n```bash\nbun add ts-explicit-errors\n# npm install ts-explicit-errors\n```\n\n## Usage\n\n```ts\nimport type { Result } from \"ts-explicit-errors\"\n\nimport { attempt, err, isErr } from \"ts-explicit-errors\"\n\nfunction getUserById(id: number): Result\u003cUser\u003e {\n  // Use the attempt function to handle potential thrown errors from external code\n  const result = attempt(() =\u003e db.findUser(id))\n  // pretend that db.findUser throws an Error with the message: \"failed to connect to database\"\n\n  if (isErr(result)) return err(\"failed to find user\", result)\n\n  return result\n}\n\nconst result = getUserById(123)\nif (isErr(result)) console.error(result.fmtErr())\n// \"failed to find user -\u003e failed to connect to database\"\nelse console.log(`Hello, ${result.name}!`)\n```\n\nYou can also add context to errors to help with debugging and logging:\n\n```ts\nfunction getUserById(id: number): Result\u003cUser\u003e {\n  const result = attempt(() =\u003e db.findUser(id))\n\n  if (isErr(result)) {\n    // Add context to the error before returning it\n    return err(\"failed to find user\", result).ctx({\n      userId: id,\n      operation: \"findUser\",\n      timestamp: new Date().toISOString(),\n    })\n  }\n\n  return result\n}\n\nconst result = getUserById(123)\nif (isErr(result)) {\n  console.error(result.fmtErr()) // \"failed to find user -\u003e failed to connect to database\"\n\n  // Get the context for logging or debugging\n  console.log(`Error for user ID: ${result.get(\"userId\")}`)\n  console.log(`Operation: ${result.get(\"operation\")}`)\n  console.log(`Timestamp: ${result.get(\"timestamp\")}`)\n} else console.log(`Hello, ${result.name}!`)\n```\n\nErrors are propagated up the call stack which helps build more useful error messages.\n\nIf applied correctly and consistently, all errors throughout your codebase are checked and handled immediately.\n\nPlease see the [API](#api) description for more details and examples. Also see [Example](#example) for a more in-depth example.\n\n## Rationale\n\nMany modern programming languages treat errors as values, and for good reason. It leads to more reliable and maintainable code by forcing error handling to be explicit. In other words, it almost completely eliminates runtime crashes due to unhandled exceptions, which is a very common problem in JS/TS.\n\nAs an alternative, there are many other libraries available that are inspired by Rust's `Result` type. Just to name a few:\n\n- [supermacro/neverthrow](https://github.com/supermacro/neverthrow)\n- [vultix/ts-results](https://github.com/vultix/ts-results)\n- [badrap/result](https://github.com/badrap/result)\n- [everweij/typescript-result](https://github.com/everweij/typescript-result)\n\nHowever, these libraries tend to be considerably more complex and have a much larger API surface. In contrast, `ts-explicit-errors` is only ~70 lines of code.\n\n## API\n\nIn an effort to keep the API concise, `ts-explicit-errors` only exports a few things:\n\n- `Result` Type\n- `isErr` Function\n- `attempt` Function\n- `err` Function\n  - The `CtxError` Class is also exported but should generally be accessed via `err`\n- `errWithCtx` Function\n\n---\n\n### `Result` Type\n\n```ts\ntype Result\u003cT = void\u003e = T | CtxError\n```\n\n`Result` represents a value or a `CtxError`.\n\nThe main idea is that **when you would normally write a function that returns `T`, you should instead return `Result\u003cT\u003e`.**\n\n- If your function doesn't return anything (i.e. `undefined`) other than an error, you can use `Result` without a type argument since `void` is the default\n\n  ```ts\n  function validateData(data: string): Result {\n    if (data.length \u003c 10) return err(\"data is too short\")\n    // use data...\n  }\n\n  const error = validateData(\"short\")\n  // Using `isErr` would be redundant here since this is either `undefined` or a `CtxError`, so a truthy check is all that is needed\n  if (error) // handle the `CtxError`\n  ```\n\nInstead of this:\n\n```ts\nfunction divide(a: number, b: number): number {\n  if (b === 0) {\n    throw new Error(\"division by zero\")\n  }\n  return a / b\n}\n\ntry {\n  const result = divide(10, 0)\n  console.log(result)\n} catch (err) {\n  console.error(\"failed to divide\", err)\n}\n```\n\nUse `Result`:\n\n```ts\nfunction divide(a: number, b: number): Result\u003cnumber\u003e {\n  if (b === 0) {\n    return err(\"division by zero\")\n  }\n  return a / b\n}\n\nconst result = divide(10, 0)\nif (isErr(result)) {\n  console.error(result.fmtErr(\"failed to divide\")) //  \"failed to divide -\u003e division by zero\"\n} else {\n  console.log(result)\n}\n```\n\n---\n\n### `isErr` Function\n\n```ts\nfunction isErr\u003cT\u003e(result: Result\u003cT\u003e): result is CtxError\n```\n\n`isErr` checks if a value is an instance of `CtxError`.\n\n- This is a wrapper around `result instanceof CtxError` to make type narrowing more concise\n\n```ts\nconst result = attempt(() =\u003e mayThrow())\nif (isErr(result)) {\n  // result is a CtxError\n  console.error(result.fmtErr())\n} else {\n  // result is of type T\n  console.log(result)\n}\n```\n\nIf you have a function that returns `Result\u003cvoid\u003e` (the default when `Result` is not given a type argument), you don't need to use `isErr`.\n\n```ts\nfunction validateData(data: string): Result {\n  if (data.length \u003c 10) return err(\"data is too short\")\n}\n\nconst result = validateData(\"short\")\n// Redundant\nif (isErr(result)) console.error(result.fmtErr())\n```\n\nIn this case, `result` is either `undefined` or a `CtxError`, so a truthy check is all that is needed.\n\n```ts\n// Note how we also name this `error` instead of `result` which is a bit more clear\nconst error = validateData(\"short\")\nif (error) console.error(error.fmtErr())\n```\n\n---\n\n### `attempt` Function\n\n`attempt` executes a function, _catches any errors thrown_, and returns a `Result`.\n\n**It is generally used for functions that _you don't control_ which might throw an error**.\n\n- Use `attempt` to \"force\" functions to return a `Result` so error handling remains consistent\n- Another way to think about this is that `attempt` should be used as far down the call stack as possible so that thrown errors are handled at their source\n\n```ts\n// in some function that returns a Result\nconst result = attempt(() =\u003e fs.readFileSync(\"non-existent.json\"))\nif (isErr(result)) {\n  return err(\"failed to read file\", result)\n}\n// do something with `result`\n```\n\nThe function can be either synchronous or asynchronous.\n\n- If the function is async / returns a Promise, the returned `Result` will be a `Promise` and should be `await`ed\n\n```ts\n// fs.readFile returns a Promise\nconst result = await attempt(() =\u003e fs.readFile(\"file.txt\"))\n```\n\n---\n\n### `err` Function\n\n```ts\nerr(message: string, cause?: unknown): CtxError\n```\n\n`err` takes a message and a cause (optional) and returns a new [`CtxError`](#ctxerror-class).\n\n- This is a wrapper around `new CtxError()` to make creating errors more concise\n\n```ts\nconst error = err(\"something went wrong\", originalError)\n```\n\nEquivalent to:\n\n```ts\nconst error = new CtxError(\"something went wrong\", { cause: originalError })\n```\n\n---\n\n### `CtxError` Class\n\n`CtxError` is a custom error class that extends the built-in `Error` class with additional functionality.\n\n- All instances of `CtxError` are instances of `Error`, so using them in place of `Error` won't cause any issues.\n\n#### `ctx` Method\n\n```ts\nctx(context: Record\u003cstring, unknown\u003e): CtxError\n```\n\nAdds context to the error.\n\n```ts\nconst error = err(\"failed to process request\").ctx({ requestId: \"abc-123\" })\n\nconsole.log(error.context) // { requestId: \"abc-123\" }\n```\n\nIf the error already has context, the new context will be merged over the existing context.\n\n```ts\nconst error = err(\"failed to process request\").ctx({ requestId: \"abc-123\", userId: 123 })\n// ... later on\nerror.ctx({ requestId: \"cba-321\" })\n\nconsole.log(error.context) // { requestId: \"cba-321\", userId: 123 }\n```\n\n#### `get` Method\n\n```ts\nget\u003cT\u003e(key: string): T | undefined\n```\n\nRetrieves a context value from the error chain (this error and all its causes), prioritizing the deepest value.\n\n```ts\n// imagine these errors are propagated up through various function calls\nconst deepError = err(\"failed to connect to database\").ctx({ logScope: \"database\" })\nconst middleError = err(\"failed to do the thing\", deepError).ctx({ logScope: \"service\" })\nconst topError = err(\"failed to process request\", middleError).ctx({ logScope: \"controller\" })\n\nconsole.log(topError.get(\"logScope\")) // \"database\" (from deepError)\n```\n\n#### `getAll` Method\n\n```ts\ngetAll\u003cT\u003e(key: string): T[]\n```\n\nRetrieves all context values as an array for a given key from the entire error chain (this error and all its causes).\n\n- Values are returned in order from shallowest (this error) to deepest (root cause)\n- Unlike `get` which returns the deepest value, this returns all values as an array\n\n```ts\n// imagine these errors are propagated up through various function calls\nconst deepError = err(\"failed to connect to database\").ctx({ logScope: \"database\" })\nconst middleError = err(\"failed to do the thing\", deepError).ctx({ logScope: \"service\" })\nconst topError = err(\"failed to process request\", middleError).ctx({ logScope: \"controller\" })\n\nconsole.log(topError.getAll(\"logScope\")) // [\"controller\", \"service\", \"database\"]\n```\n\n#### `fmtErr` Method\n\n```ts\nfmtErr(message?: string): string\n```\n\nReturns a nicely formatted error message by unwrapping all error messages in the error chain (this error and all its causes).\n\n- The error message is formatted as: `message -\u003e cause1 -\u003e cause2 -\u003e ... -\u003e causeN`\n- An optional message can be provided which will be the first message in the string\n\n```ts\n// imagine these errors are propagated up through various function calls\nconst deepError = err(\"failed to connect to database\")\nconst middleError = err(\"failed to do the thing\", deepError)\nconst topError = err(\"failed to process request\", middleError)\n\nconsole.log(topError.fmtErr())\n// \"failed to process request -\u003e failed to do the thing -\u003e failed to connect to database\"\n\n// With the optional message given\nconsole.log(topError.fmtErr(\"something went wrong\"))\n// \"something went wrong -\u003e failed to process request -\u003e failed to do the thing -\u003e failed to connect to database\"\n```\n\n---\n\n### `errWithCtx` Function\n\n```ts\nfunction errWithCtx(defaultContext: Record\u003cstring, unknown\u003e): (message: string, cause?: unknown) =\u003e CtxError\n```\n\nCreates a [`err`](#err-function) function with predefined context. This is useful when you want to create multiple errors with the same context, such as a common scope or component name.\n\n```ts\nconst serviceErr = errWithCtx({ scope: \"userService\" })\n\nfunction getUserById(id: number): Result\u003cUser\u003e {\n  const result = attempt(() =\u003e db.findUser(id))\n\n  // No need to manually add the scope context every time\n  if (isErr(result)) return serviceErr(\"failed to find user\", result)\n\n  return result\n}\n\n// The error will automatically have { scope: \"userService\" } in its context\n```\n\n---\n\n## Example\n\nThis example is a bit contrived, but use your imagination :)\n\nPutting it all together:\n\n```ts\nimport type { Result } from \"ts-explicit-errors\"\n\nimport { attempt, err, isErr } from \"ts-explicit-errors\"\n\n// Pretend this \"db\" module doesn't belong to us and its functions might throw errors\nconst db = {\n  connect: (dbId: string) =\u003e {\n    throw new Error(\"invalid dbId\")\n  },\n  query: (queryString: string) =\u003e {\n    throw new Error(\"invalid query\")\n  },\n}\n\n// This function doesn't return a value (other than a possible error), so we use `Result` without a type argument\nfunction connectToDb(dbId: string): Result {\n  const result = attempt(() =\u003e db.connect(dbId))\n  if (isErr(result)) {\n    return err(\"failed to connect to database\", result).ctx({\n      timestamp: \"\u003ctimestamp1\u003e\", // pretend this is something like new Date().toISOString()\n      logScope: \"connect\",\n    })\n  }\n}\n\n// A function that returns a `Result` of a specific type\nasync function queryDb(queryString: string): Promise\u003cResult\u003cDbQuery\u003e\u003e {\n  const connectToDbError = connectToDb(\"db-prod-1\")\n  // Using `isErr` would be redundant here since `connectToDbError` is either `undefined` or a `CtxError`, so a truthy check is all that is needed\n  if (connectToDbError) {\n    // We don't need to provide an additional message or context here, so we return the error directly\n    return connectToDbError\n  }\n\n  const queryResult = await attempt(() =\u003e db.query(queryString))\n  if (isErr(queryResult)) {\n    return err(\"failed to query db\", queryResult).ctx({ queryString, logScope: \"query\", timestamp: \"\u003ctimestamp2\u003e\" })\n  }\n\n  return queryResult\n}\n\nasync function main(): Promise\u003cResult\u003cMeeting[]\u003e\u003e {\n  const meetingsQueryResult = await queryDb(\"SELECT * FROM meetings WHERE scheduled_time \u003c actual_end_time\")\n  if (isErr(meetingsQueryResult)) {\n    return err(\"failed to get meetings\", meetingsQueryResult).ctx({ logScope: \"main\" })\n  }\n\n  return meetingsQueryResult\n}\n\nconst result = await example()\nif (isErr(result)) {\n  const fullContext = {\n    logScope: result.getAll\u003cstring\u003e(\"logScope\").join(\"|\"),\n    timestamp: result.get\u003cstring\u003e(\"timestamp\") ?? \"\",\n    queryString: result.get\u003cstring\u003e(\"queryString\") ?? \"\",\n  }\n\n  logger.error(result.fmtErr(\"something went wrong\"), fullContext)\n} else console.log(result)\n```\n\nIn the above, let's say we have some `logger` that is able to handle our context. We'll pretend that it prefixes our error message with the context as a nicely formatted string. e.g. `${timestamp} [${logScope}]`\n\nLooking at the example, we have two situations where there could be an error: 1. `db.connect()` 2. `db.query()`\n\nLet's see what each error would look like:\n\nFor `db.connect()`:\n\n```\n\u003ctimestamp1\u003e [main|connect] something went wrong -\u003e failed to get meetings -\u003e failed to connect to database -\u003e invalid dbId\n```\n\nFor `db.query()`:\n\n```\n\u003ctimestamp2\u003e [main|query] something went wrong -\u003e failed to get meetings -\u003e failed to query db -\u003e invalid query: for 'SELECT * FROM meetings WHERE scheduled_time \u003c actual_end_time'\n```\n\nIn this case, our logger appended `: for '${queryString}'`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamhl8%2Fts-explicit-errors","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadamhl8%2Fts-explicit-errors","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamhl8%2Fts-explicit-errors/lists"}