{"id":39426574,"url":"https://github.com/Snowflyt/tinyeffect","last_synced_at":"2026-01-26T10:00:51.059Z","repository":{"id":260909256,"uuid":"882700065","full_name":"Snowflyt/tinyeffect","owner":"Snowflyt","description":"A tiny TypeScript library for handling side effects in a unified way using algebraic effects, offering a type-safe approach for async operations, error handling, dependency injection, and more.","archived":false,"fork":false,"pushed_at":"2025-03-30T05:34:43.000Z","size":1597,"stargazers_count":30,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-31T00:33:44.935Z","etag":null,"topics":["algebraic-effects","side-effect","side-effects","typesafe","typesafety"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/tinyeffect","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/Snowflyt.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,"zenodo":null}},"created_at":"2024-11-03T14:32:12.000Z","updated_at":"2025-08-08T08:51:01.000Z","dependencies_parsed_at":null,"dependency_job_id":"3576f37a-bb2f-415f-95bc-bbd0ea2688cc","html_url":"https://github.com/Snowflyt/tinyeffect","commit_stats":null,"previous_names":["snowflyt/tinyeffect"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/Snowflyt/tinyeffect","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftinyeffect","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftinyeffect/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftinyeffect/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftinyeffect/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Snowflyt","download_url":"https://codeload.github.com/Snowflyt/tinyeffect/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Snowflyt%2Ftinyeffect/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28774297,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-26T09:42:00.929Z","status":"ssl_error","status_checked_at":"2026-01-26T09:42:00.591Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: 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":["algebraic-effects","side-effect","side-effects","typesafe","typesafety"],"created_at":"2026-01-18T04:00:25.862Z","updated_at":"2026-01-26T10:00:51.040Z","avatar_url":"https://github.com/Snowflyt.png","language":"TypeScript","readme":"\u003ch1 align=\"center\"\u003etinyeffect\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n\u003cstrong\u003eAlgebraic effects\u003c/strong\u003e, in \u003cstrong\u003eTypeScript\u003c/strong\u003e.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\nHandle side effects in a \u003cstrong\u003eunified\u003c/strong\u003e way, with \u003cstrong\u003etype-safety\u003c/strong\u003e and elegance.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/tinyeffect\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/v/tinyeffect.svg\" alt=\"npm version\" height=\"18\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://bundlephobia.com/package/tinyeffect\"\u003e\n    \u003cimg src=\"https://img.shields.io/bundlephobia/minzip/tinyeffect.svg\" alt=\"minzipped size\" height=\"18\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/Snowflyt/tinyeffect/actions/workflows/ci.yml\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/actions/workflow/status/Snowflyt/tinyeffect/ci.yml?label=test\" alt=\"test status\" height=\"18\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://coveralls.io/github/Snowflyt/tinyeffect?branch=main\"\u003e\n    \u003cimg src=\"https://coveralls.io/repos/github/Snowflyt/tinyeffect/badge.svg?branch=main\" alt=\"coverage status\" height=\"18\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/Snowflyt/tinyeffect\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/l/tinyeffect.svg\" alt=\"MIT license\" height=\"18\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n![screenshot](./screenshot.svg)\n\n## About\n\nProgramming heavily relies on **side effects**. Imagine a program without I/O capabilities (even basic console access) — it would be pretty useless.\n\nThere are many kinds of side effects: **I/O operations**, **error handling**, **dependency injection**, **asynchronous operations**, logging, and so on.\n\nHowever, some effects don’t fit well with TypeScript’s type system — for example, error handling, where try-catch blocks only capture unknown types. Other effects, such as asynchronous operations, come with inherent challenges like asynchronous contagion.\n\nThat’s where tinyeffect comes in.\n\ntinyeffect provides a **unified** way to handle _all_ these side effects in a **type-safe** manner. It’s a tiny yet powerful library with its core logic implemented in only around 400 lines of code. The idea is inspired by the effect system from the [Koka](https://koka-lang.github.io/koka/doc/book.html#why-handlers) language. It uses **algebraic effects** to model side effects, which are then handled by effect handlers.\n\n_Don’t worry_ if you are not familiar with these concepts. tinyeffect is designed to be simple and easy to use. You can start using it right away without knowing the underlying theory. Simply start with the [Usage](#usage) section to see it in action.\n\n## Installation\n\n```shell\nnpm install tinyeffect\n```\n\n## Usage\n\nConsider a simple example. Imagine a function that handles `POST /api/users` requests in a backend application. The function needs to:\n\n- Retrieve the current logged-in user from the context.\n- Check if the user has permission to create a new user (in this case, only admin users can create new users). If not, an error is thrown.\n- If the user has the necessary permission, create a new user in the database.\n\nThis example demonstrates three types of side effects: dependency injection (retrieving the current user), error handling (checking permissions), and asynchronous operations (database operations). Here’s how these side effects can be handled using tinyeffect and TypeScript:\n\n```typescript\nimport { dependency, effect, effected, error } from \"tinyeffect\";\n\ntype User = { id: number; name: string; role: \"admin\" | \"user\" };\n\nconst println = effect(\"println\")\u003cunknown[], void\u003e;\nconst executeSQL = effect(\"executeSQL\")\u003c[sql: string, ...params: unknown[]], any\u003e;\nconst askCurrentUser = dependency(\"currentUser\")\u003cUser | null\u003e;\nconst authenticationError = error(\"authentication\");\nconst unauthorizedError = error(\"unauthorized\");\n\nconst requiresAdmin = () =\u003e effected(function* () {\n  const currentUser = yield* askCurrentUser();\n  if (!currentUser) return yield* authenticationError();\n  if (currentUser.role !== \"admin\")\n    return yield* unauthorizedError(`User \"${currentUser.name}\" is not an admin`);\n});\n\nconst createUser = (user: Omit\u003cUser, \"id\"\u003e) =\u003e effected(function* () {\n  yield* requiresAdmin();\n  const id = yield* executeSQL(\"INSERT INTO users (name) VALUES (?)\", user.name);\n  const savedUser: User = { id, ...user };\n  yield* println(\"User created:\", savedUser);\n  return savedUser;\n});\n```\n\nThe code above defines five effects: `println`, `executeSQL`, `currentUser`, `authentication`, and `unauthorized`. Effects can be defined using `effect`, with `dependency` and `error` as wrappers for specific purposes.\n\nYou can define effected programs using the `effected` function together with a generator function. Inside the generator, simply write the program as if it were a normal synchronous one — just add some `yield*` where you perform effects or other effected programs.\n\nHovering over the `requiresAdmin` and `createUser` functions in your editor reveals their type signatures:\n\n```typescript\nconst requiresAdmin: () =\u003e Effected\u003c\n  | Unresumable\u003cEffect\u003c\"error:authentication\", [message?: string], never\u003e\u003e\n  | Effect\u003c\"dependency:currentUser\", [], User | null\u003e\n  | Unresumable\u003cEffect\u003c\"error:unauthorized\", [message?: string], never\u003e\u003e,\n  undefined\n\u003e;\n\nconst createUser: (\n  user: Omit\u003cUser, \"id\"\u003e,\n) =\u003e Effected\u003c\n  | Unresumable\u003cEffect\u003c\"error:authentication\", [message?: string], never\u003e\u003e\n  | Effect\u003c\"dependency:currentUser\", [], User | null\u003e\n  | Unresumable\u003cEffect\u003c\"error:unauthorized\", [message?: string], never\u003e\u003e\n  | Effect\u003c\"executeSQL\", [sql: string, ...params: unknown[]], any\u003e\n  | Effect\u003c\"println\", unknown[], void\u003e,\n  User\n\u003e;\n```\n\nThe inferred type signature shows which effects the program can perform and its return value. We’ll dive into the `Effect` type in detail later, but for now, let’s explore handling these effects:\n\n```typescript\nconst alice: Omit\u003cUser, \"id\"\u003e = { name: \"Alice\", role: \"user\" };\n\nconst handled = createUser(alice).handle(\"executeSQL\", ({ resume }, sql, ...params) =\u003e {\n  console.log(`Executing SQL: ${sql}`);\n  console.log(\"Parameters:\", params);\n  resume(42);\n});\n```\n\nWe can invoke `.handle()` on an effected program to handle its effects. The first argument is the effect name, and the second is a handler function. The handler receives an object with two functions: `resume` and `terminate`, plus any parameters passed to the effect. You can use `resume` to continue the program with a value or `terminate` to halt and return a value immediately as the program’s result.\n\nSince `createUser` is a function that _returns_ an effected program, we first need to invoke `createUser` with `alice` to obtain this program, which then allows us to call `.handle()` on it to handle its effects, such as `executeSQL`. Note that while `alice` is passed to `createUser`, the program itself still won’t execute at this point. Only after handling all effects will we use `.runSync()` or `.runAsync()` to actually execute it, which will be covered later.\n\nHovering over `handled` in your editor reveals its type signature:\n\n```typescript\nconst handled: Effected\u003c\n  | Unresumable\u003cEffect\u003c\"error:authentication\", [message?: string], never\u003e\u003e\n  | Effect\u003c\"dependency:currentUser\", [], User | null\u003e\n  | Unresumable\u003cEffect\u003c\"error:unauthorized\", [message?: string], never\u003e\u003e\n  | Effect\u003c\"println\", unknown[], void\u003e,\n  User\n\u003e;\n```\n\nLet’s handle the rest of the effects.\n\n```typescript\nconst handled = createUser(alice)\n  .handle(\"executeSQL\", ({ resume }, sql, ...params) =\u003e {\n    console.log(`Executing SQL: ${sql}`);\n    console.log(\"Parameters:\", params);\n    resume(42);\n  })\n  .handle(\"println\", ({ resume }, ...args) =\u003e {\n    console.log(...args);\n    resume();\n  })\n  .handle(\"dependency:currentUser\", ({ resume }) =\u003e {\n    resume({ id: 1, name: \"Charlie\", role: \"admin\" });\n  })\n  .handle\u003c\"error:authentication\", void\u003e(\"error:authentication\", ({ terminate }) =\u003e {\n    console.error(\"Authentication error\");\n    terminate();\n  })\n  .handle\u003c\"error:unauthorized\", void\u003e(\"error:unauthorized\", ({ terminate }) =\u003e {\n    console.error(\"Unauthorized error\");\n    terminate();\n  });\n```\n\nFor error effects, we specify the return type (`void`) with a type argument for `.handle()` since `terminate` can end the program with any value, and TypeScript can’t infer this type automatically.\n\nAfter handling all effects, the `handled` variable’s type signature becomes:\n\n```typescript\nconst handled: Effected\u003cnever, void | User\u003e;\n```\n\nWe get `never` as the effect list because all effects have been handled. The return type becomes `void | User` because the program may terminate with `void` (due to terminate in error handlers) or return a `User` value.\n\nLet’s run the program. Since all operations are synchronous in this example, we can use `.runSync()`:\n\n```typescript\nhandled.runSync();\n// Executing SQL: INSERT INTO users (name) VALUES (?)\n// Parameters: [ 'Alice' ]\n// User created: { id: 42, name: 'Alice', role: 'user' }\n```\n\nWhat happens if we don’t handle all the effects? Let’s remove some handlers, e.g., the `println` and `currentUser` handlers. Now TypeScript will give you an error:\n\n```typescript\nhandled.runSync();\n//      ~~~~~~~\n// This expression is not callable.\n//   Type 'UnhandledEffect\u003cEffect\u003c\"dependency:currentUser\", [], User | null\u003e | Effect\u003c\"println\", unknown[], void\u003e\u003e' has no call signatures.\n```\n\nIf you ignore this compile-time error, you’ll still encounter a runtime error:\n\n```typescript\nhandled.runSync();\n// UnhandledEffectError: Unhandled effect: dependency:currentUser()\n//     at runSync (...)\n```\n\n\u003e [!TIP]\n\u003e\n\u003e You can access the unhandled effect by using the `.effect` property of the error object:\n\u003e\n\u003e ```typescript\n\u003e import { UnhandledEffectError } from \"tinyeffect\";\n\u003e\n\u003e try {\n\u003e   handled.runSync();\n\u003e } catch (e) {\n\u003e   if (e instanceof UnhandledEffectError) console.error(`Unhandled effect: ${e.effect.name}`);\n\u003e }\n\u003e ```\n\ntinyeffect provides concise variants of `.handle()` to streamline effect handling. These include `.resume()` and `.terminate()`, which use the handler’s return value to resume or terminate the program, respectively. Special effects, like errors (names prefixed with `\"error:\"`) and dependencies (names prefixed with `\"dependency:\"`), use `.catch()`, `.provide()`, and `.provideBy()` for specialized handling.\n\nLet’s see how we can rewrite the previous example using these variants:\n\n```typescript\nconst handled = createUser(alice)\n  .resume(\"executeSQL\", (sql, ...params) =\u003e {\n    console.log(`Executing SQL: ${sql}`);\n    console.log(\"Parameters:\", params);\n    return 42;\n  })\n  .resume(\"println\", console.log)\n  .provide(\"currentUser\", { id: 1, name: \"Charlie\", role: \"admin\" })\n  .catch(\"authentication\", () =\u003e console.error(\"Authentication error\"))\n  .catch(\"unauthorized\", () =\u003e console.error(\"Unauthorized error\"));\n```\n\nWhat about asynchronous operations? Typically, operations like `executeSQL` are asynchronous, as they involve I/O and wait for a result. In tinyeffect, synchronous and asynchronous operations are not distinguished, so you can simply call `resume` or `terminate` inside an asynchronous callback. Here’s how to handle an asynchronous operation:\n\n```typescript\nconst handled = createUser(alice)\n  .handle(\"executeSQL\", ({ resume }, sql, ...params) =\u003e {\n    console.log(`Executing SQL: ${sql}`);\n    console.log(\"Parameters:\", params);\n    // Simulate async operation\n    setTimeout(() =\u003e {\n      console.log(`SQL executed`);\n      resume(42);\n    }, 100);\n  })\n  .resume(\"println\", console.log)\n  .provide(\"currentUser\", { id: 1, name: \"Charlie\", role: \"admin\" })\n  .catch(\"authentication\", () =\u003e console.error(\"Authentication error\"))\n  .catch(\"unauthorized\", () =\u003e console.error(\"Unauthorized error\"));\n```\n\nYou can then run the program using `.runAsync()`:\n\n```typescript\nawait handled.runAsync();\n// Executing SQL: INSERT INTO users (name) VALUES (?)\n// Parameters: [ 'Alice' ]\n// SQL executed\n// User created: { id: 42, name: 'Alice', role: 'user' }\n```\n\nIf you run an effected program with asynchronous operations using `.runSync()`, the program will throw an error:\n\n```typescript\nhandled.runSync();\n// Error: Cannot run an asynchronous effected program with `runSync`, use `runAsync` instead\n//     at runSync (...)\n```\n\nSadly, synchronous and asynchronous effected programs can’t be distinguished at compile-time, so running an asynchronous program with `.runSync()` won’t raise a compile-time error but may fail at runtime. In most cases, though, this shouldn’t be an issue: if you’re building an application with tinyeffect, you’re likely invoking `.runSync()` or `.runAsync()` only at the entry point, so there should be only a few places where you need to be careful about this.\n\n\u003e [!TIP]\n\u003e\n\u003e You can use `.runSyncUnsafe()` or `.runAsyncUnsafe()` to run the program without handling all effects. This is useful for testing environments, situations where unhandled effects are not a concern, or cases where you’re certain all effects are handled correctly but TypeScript hasn’t inferred the types as expected.\n\ntinyeffect integrates seamlessly with common APIs that use async/await syntax. For example, say you’re using an API like `db.user.create(user: User): Promise\u003cnumber\u003e` to create a user in the database. You might want to write code like this:\n\n```typescript\n// prettier-ignore\nconst createUser = (user: Omit\u003cUser, \"id\"\u003e) =\u003e effected(function* () {\n  yield* requiresAdmin();\n  const id = await db.user.create(user);\n  const savedUser: User = { id, ...user };\n  yield* println(\"User created:\", savedUser);\n  return savedUser;\n});\n```\n\nSince `await` cannot be used inside a generator function, you might instead create a special effect (e.g., `createUser`) and handle it later with `db.user.create.then(resume)`, though this approach can be awkward. To address this, tinyeffect provides `effectify`, a helper function that transforms a `Promise` into an effected program, allowing `yield*` in place of `await`:\n\n```typescript\nimport { effectify } from \"tinyeffect\";\n\n// prettier-ignore\nconst createUser = (user: Omit\u003cUser, \"id\"\u003e) =\u003e effected(function* () {\n  yield* requiresAdmin();\n  const id = yield* effectify(db.user.create(user));\n  const savedUser = { id, ...user };\n  yield* println(\"User created:\", savedUser);\n  return savedUser;\n});\n```\n\nThis knowledge is enough to get started with tinyeffect in your projects. For more advanced features, refer to the sections below. Now, let’s combine the code snippets above into a complete example to recap:\n\n```typescript\nimport { dependency, effect, effected, error } from \"tinyeffect\";\n\ntype User = { id: number; name: string; role: \"admin\" | \"user\" };\n\nconst println = effect(\"println\")\u003cunknown[], void\u003e;\nconst executeSQL = effect(\"executeSQL\")\u003c[sql: string, ...params: unknown[]], any\u003e;\nconst askCurrentUser = dependency(\"currentUser\")\u003cUser | null\u003e;\nconst authenticationError = error(\"authentication\");\nconst unauthorizedError = error(\"unauthorized\");\n\nconst requiresAdmin = () =\u003e\n  effected(function* () {\n    const currentUser = yield* askCurrentUser();\n    if (!currentUser) return yield* authenticationError();\n    if (currentUser.role !== \"admin\")\n      return yield* unauthorizedError(`User \"${currentUser.name}\" is not an admin`);\n  });\n\nconst createUser = (user: Omit\u003cUser, \"id\"\u003e) =\u003e\n  effected(function* () {\n    yield* requiresAdmin();\n    const id = yield* executeSQL(\"INSERT INTO users (name) VALUES (?)\", user.name);\n    const savedUser: User = { id, ...user };\n    yield* println(\"User created:\", savedUser);\n    return savedUser;\n  });\n\n/* Example */\nlet sqlId = 1;\n\nconst program = effected(function* () {\n  yield* createUser({ name: \"Alice\", role: \"user\" });\n  yield* createUser({ name: \"Bob\", role: \"admin\" });\n})\n  .resume(\"println\", (...args) =\u003e {\n    console.log(...args);\n  })\n  .handle(\"executeSQL\", ({ resume }, sql, ...params) =\u003e {\n    console.log(`[${sqlId}] Executing SQL: ${sql}`);\n    console.log(`[${sqlId}] Parameters: ${params.join(\", \")}`);\n    // Simulate async operation\n    setTimeout(() =\u003e {\n      console.log(`[${sqlId}] SQL executed`);\n      sqlId++;\n      resume(sqlId);\n    }, 100);\n  })\n  .provide(\"currentUser\", { id: 1, name: \"Charlie\", role: \"admin\" })\n  .catch(\"authentication\", () =\u003e {\n    console.error(\"Authentication error\");\n  })\n  .catch(\"unauthorized\", () =\u003e {\n    console.error(\"Unauthorized error\");\n  });\n\nawait program.runAsync();\n```\n\nAfter running the program, you should see the following output:\n\n```text\n[1] Executing SQL: INSERT INTO users (name) VALUES (?)\n[1] Parameters: Alice\n[1] SQL executed\nUser created: { id: 2, name: 'Alice', role: 'user' }\n[2] Executing SQL: INSERT INTO users (name) VALUES (?)\n[2] Parameters: Bob\n[2] SQL executed\nUser created: { id: 3, name: 'Bob', role: 'admin' }\n```\n\n### The `Effect` type\n\ntinyeffect is all around effects. The `Effect` type is the core type that represents an effect. The type itself is straightforward: the first parameter specifies the effect’s name, the second defines its parameters, and the third denotes its return type. Here’s how `Effect` is defined (a simplified version of the actual type signature):\n\n```typescript\nexport interface Effect\u003c\n  out Name extends string | symbol = string | symbol,\n  out Payloads extends unknown[] = unknown[],\n  out R = unknown,\n\u003e {\n  readonly name: Name;\n  readonly payloads: Payloads;\n  readonly __returnType: R;\n}\n```\n\nAt runtime, only `name` and `payloads` are present, while `__returnType` serves purely at the type level to infer the effect’s return type.\n\nUsing `yield*` with a factory function created by `effect` (or its variants) yields an `Effect` object. To understand how it works, let’s take a look at a simplified version of the `effect` function (and its variants) in JavaScript:\n\n```typescript\nfunction effect(name) {\n  return function* (...payloads) {\n    return yield { name, payloads };\n  };\n}\n\nfunction error(name) {\n  return function* (...payloads) {\n    return yield { name: `error:${name}`, payloads, resumable: false };\n  };\n}\n\nfunction dependency(name) {\n  return function* () {\n    return yield { name: `dependency:${name}`, payloads: [] };\n  };\n}\n```\n\nWhile the actual implementation of `effect` is more complex and returns a factory function that produces an `Effected` instance (instead of a generator function), the fundamental concept remains the same.\n\nThe mechanism of `.runSync()` and `.runAsync()` is also straightforward. These methods iterate through the generator function, and when encountering an `Effect` object, invoke the corresponding handler registered by `.handle()`. They then either resume or terminate the generator with the value passed to `resume` or `terminate`. The actual implementation is more complex, but the concept remains consistent.\n\n\u003e [!NOTE]\n\u003e\n\u003e Effect names are used to distinguish between different effects, so reusing the same name for different effects may cause conflicts:\n\u003e\n\u003e ```typescript\n\u003e const effectA = effect(\"foo\");\n\u003e const programA = effected(function* () {\n\u003e   return yield* effectA();\n\u003e });\n\u003e\n\u003e const effectB = effect(\"foo\"); // Same name as effectA\n\u003e const programB = effected(function* () {\n\u003e   return yield* effectB();\n\u003e });\n\u003e\n\u003e effected(function* () {\n\u003e   console.log(yield* programA);\n\u003e   console.log(yield* programB);\n\u003e })\n\u003e   .resume(\"foo\", () =\u003e 42)\n\u003e   .runSync();\n\u003e // Will log 42 twice\n\u003e ```\n\u003e\n\u003e However, once an effect is handled, it’s “hidden” from the program, so the same name can be reused in different parts:\n\u003e\n\u003e ```typescript\n\u003e const effectA = effect(\"foo\");\n\u003e const programA = effected(function* () {\n\u003e   return yield* effectA();\n\u003e }).resume(\"foo\", () =\u003e 21);\n\u003e\n\u003e const effectB = effect(\"foo\");\n\u003e const programB = effected(function* () {\n\u003e   return yield* effectB();\n\u003e });\n\u003e\n\u003e effected(function* () {\n\u003e   console.log(yield* programA);\n\u003e   console.log(yield* programB);\n\u003e })\n\u003e   .resume(\"foo\", () =\u003e 42)\n\u003e   .runSync();\n\u003e // Will log 21 and 42 respectively\n\u003e ```\n\u003e\n\u003e You can also use symbols for effect names to avoid conflicts:\n\u003e\n\u003e ```typescript\n\u003e const nameA = Symbol(\"nameA\");\n\u003e const effectA = effect(nameA);\n\u003e const programA = effected(function* () {\n\u003e   return yield* effectA();\n\u003e });\n\u003e\n\u003e const nameB = Symbol(\"nameB\");\n\u003e const effectB = effect(nameB);\n\u003e const programB = effected(function* () {\n\u003e   return yield* effectB();\n\u003e });\n\u003e\n\u003e effected(function* () {\n\u003e   console.log(yield* programA);\n\u003e   console.log(yield* programB);\n\u003e })\n\u003e   .resume(nameA, () =\u003e 21)\n\u003e   .resume(nameB, () =\u003e 42)\n\u003e   .runSync();\n\u003e // Will log 21 and 42 respectively\n\u003e ```\n\n#### Unresumable effects\n\nYou may notice there’re several kinds of effects: effects that never resume (like errors), effects that only resume (like dependencies and `println`), and effects that can either resume or terminate (we haven’t seen this kind of effect yet, such effects may not be very common, but are useful in some cases).\n\nApparently, for effects that never resume, you should only handle them with `terminate`. You can declare such effects using the `{ resumable: false }` option in the `effect` function, and TypeScript will enforce handling them with `terminate`. For example:\n\n```typescript\nconst raise = effect(\"raise\", { resumable: false })\u003c[error: unknown], never\u003e;\n```\n\nWhen you hover over the `raise` variable, you’ll see its `Effect` type is wrapped with `Unresumable`:\n\n```typescript\nconst raise: (\n  error: unknown,\n) =\u003e Effected\u003cUnresumable\u003cEffect\u003c\"raise\", [error: unknown], never\u003e\u003e, never\u003e;\n```\n\nAttempting to handle an unresumable effect with `.resume()` will result in a TypeScript error:\n\n```typescript\neffected(function* () {\n  yield* raise(\"Something went wrong\");\n}).resume(\"raise\", console.error);\n//        ~~~~~~~\n// No overload matches this call.\n//   ...\n```\n\nIgnoring this TypeScript error would still lead to a runtime error:\n\n```typescript\neffected(function* () {\n  yield* raise(\"An error occurred\");\n})\n  .resume(\"raise\", console.error)\n  .runSync();\n// Error: Cannot resume non-resumable effect: raise(\"An error occurred\")\n//     at ...\n```\n\n#### Provide more readable type information\n\nWhen an effected program involves multiple effects, its type signature can become lengthy and difficult to read. For example, let’s look again at the type signature of the `createUser` function:\n\n```typescript\nconst createUser: (\n  user: Omit\u003cUser, \"id\"\u003e,\n) =\u003e Effected\u003c\n  | Unresumable\u003cEffect\u003c\"error:authentication\", [message?: string], never\u003e\u003e\n  | Effect\u003c\"dependency:currentUser\", [], User | null\u003e\n  | Unresumable\u003cEffect\u003c\"error:unauthorized\", [message?: string], never\u003e\u003e\n  | Effect\u003c\"executeSQL\", [sql: string, ...params: unknown[]], any\u003e\n  | Effect\u003c\"println\", unknown[], void\u003e,\n  User\n\u003e;\n```\n\nYou can make this signature much more readable by assigning names to these effects using the `Effect` type and `EffectFactory` helper type. Here’s how:\n\n```typescript\nimport { dependency, effect, effected, error } from \"tinyeffect\";\nimport type { Effect, EffectFactory } from \"tinyeffect\";\n\ntype Println = Effect\u003c\"println\", unknown[], void\u003e;\nconst println: EffectFactory\u003cPrintln\u003e = effect(\"println\");\ntype ExecuteSQL = Effect\u003c\"executeSQL\", [sql: string, ...params: unknown[]], any\u003e;\nconst executeSQL: EffectFactory\u003cExecuteSQL\u003e = effect(\"executeSQL\");\ntype CurrentUserDependency = Effect.Dependency\u003c\"currentUser\", User | null\u003e;\nconst askCurrentUser: EffectFactory\u003cCurrentUserDependency\u003e = dependency(\"currentUser\")\u003cUser | null\u003e;\ntype AuthenticationError = Effect.Error\u003c\"authentication\"\u003e;\nconst authenticationError: EffectFactory\u003cAuthenticationError\u003e = error(\"authentication\");\ntype UnauthorizedError = Effect.Error\u003c\"unauthorized\"\u003e;\nconst unauthorizedError: EffectFactory\u003cUnauthorizedError\u003e = error(\"unauthorized\");\n```\n\nNow, when you hover over the `createUser` function, you’ll see a much cleaner type signature:\n\n```typescript\nconst createUser: (\n  user: Omit\u003cUser, \"id\"\u003e,\n) =\u003e Effected\u003c\n  | Unresumable\u003cAuthenticationError\u003e\n  | CurrentUserDependency\n  | Unresumable\u003cUnauthorizedError\u003e\n  | ExecuteSQL\n  | Println,\n  User\n\u003e;\n```\n\n### A deep dive into `resume` and `terminate`\n\nLet’s take a closer look at the `resume` and `terminate` functions. `resume` resumes the program with a given value, while `terminate` stops the program with a value immediately. Use `terminate` when you need to end the program early, such as when an error occurs, while `resume` is typically used to continue normal execution.\n\nThe difference might seem obvious, but in real-world cases, the behavior can sometimes surprising you. Consider this example (adapted from the [Koka documentation](https://koka-lang.github.io/koka/doc/book.html#sec-handling)):\n\n```typescript\nconst raise = effect(\"raise\")\u003c[error: unknown], any\u003e;\n\nconst safeDivide = (a: number, b: number) =\u003e\n  effected(function* () {\n    if (b === 0) return yield* raise(\"Division by zero\");\n    return a / b;\n  });\n\nconst program = effected(function* () {\n  return 8 + (yield* safeDivide(1, 0));\n}).terminate(\"raise\", () =\u003e 42);\n```\n\nWhat would you expect the result of `program.runSync()` to be? The answer is `42` (not `50`). The `terminate` function immediately ends the program, so the `8 + ...` part is never executed.\n\nUntil now, we’ve used either `resume` or `terminate` within a handler, but it’s also possible to use both in a single handler. For example:\n\n```typescript\ntype Iterate\u003cT\u003e = Effect\u003c\"iterate\", [value: T], void\u003e;\nconst iterate = \u003cT\u003e(value: T) =\u003e effect(\"iterate\")\u003c[value: T], void\u003e(value);\n\nconst iterateOver = \u003cT\u003e(iterable: Iterable\u003cT\u003e): Effected\u003cIterate\u003cT\u003e, void\u003e =\u003e\n  effected(function* () {\n    for (const value of iterable) {\n      yield* iterate(value);\n    }\n  });\n\nlet i = 0;\nconst program = iterateOver([1, 2, 3, 4, 5]).handle(\"iterate\", ({ resume, terminate }, value) =\u003e {\n  if (i++ \u003e= 3) {\n    // Too many iterations\n    terminate();\n    return;\n  }\n  console.log(\"Iterating over\", value);\n  resume();\n});\n```\n\nIn this example, `terminate` stops the program after iterating too many times (in this case, more than 3 times), while `resume` continues the loop. Running `program.runSync()` will produce the following output:\n\n```text\nIterating over 1\nIterating over 2\nIterating over 3\n```\n\n\u003e [!NOTE]\n\u003e\n\u003e Each handler should call `resume` or `terminate` exactly once. If a handler calls either function multiple times, only the first invocation will take effect, and subsequent calls will be ignored (a warning will appear in the console). If neither function is called, the program will hang indefinitely.\n\nIn this example, we also create a generic effect `Iterate` to iterate over an iterable:\n\n```typescript\ntype Iterate\u003cT\u003e = Effect\u003c\"iterate\", [value: T], void\u003e;\nconst iterate = \u003cT\u003e(value: T) =\u003e effect(\"iterate\")\u003c[value: T], void\u003e(value);\n```\n\nThis might look complex at first, but it simply wraps the `effect` function to make it generic. `effect(\"iterate\")` returns a generic factory function that produces an `Effected` instance, and we pass type arguments to specialize it, then call the function with a value to yield an `Effect` object.\n\n### Handling effects with another effected program\n\nWhen you’re working with a large application, you’ll soon encounter situations where handling an effect can introduce one or more new effects.\n\nFor example, consider the following program:\n\n```typescript\ntype Ask\u003cT\u003e = Effect\u003c\"ask\", [], T\u003e;\nconst ask = \u003cT\u003e(): Effected\u003cAsk\u003cT\u003e, T\u003e =\u003e effect(\"ask\")();\n\nconst double = (): Effected\u003cAsk\u003cnumber\u003e, number\u003e =\u003e\n  effected(function* () {\n    return (yield* ask\u003cnumber\u003e()) + (yield* ask\u003cnumber\u003e());\n  });\n```\n\nWhat if we want to use a random number to handle the `ask` effect? We could use `Math.random()`, but generating a random number is itself a side effect, so let’s define it as an effect as well:\n\n```typescript\ntype Random = Effect\u003c\"random\", [], number\u003e;\nconst random: EffectFactory\u003cRandom\u003e = effect(\"random\");\n\nconst program = effected(function* () {\n  return yield* double();\n}).resume(\"ask\", function* () {\n  return yield* random();\n});\n```\n\nAs shown, when handling effects with other effects (or with other effected programs), you can use a _generator function_ as a handler. The `.handle()` method and its variants support generator functions, allowing you to use `yield*` inside handlers to perform additional effects.\n\nHovering over the `program` variable reveals its type signature:\n\n```typescript\nconst program: Effected\u003cRandom, number\u003e;\n```\n\nHere, the `Ask\u003cnumber\u003e` effect has been handled, but now the program has a new `Random` effect introduced by handling `ask` with `random`.\n\nYou can also “override” an effect by yielding the effect itself within the generator function. Consider this example (adapted from the [Koka documentation](https://koka-lang.github.io/koka/doc/book.html#sec-overriding-handlers)):\n\n```typescript\ntype Emit = Effect\u003c\"emit\", [msg: string], void\u003e;\nconst emit: EffectFactory\u003cEmit\u003e = effect(\"emit\");\n\nconst program = effected(function* () {\n  yield* emit(\"hello\");\n  yield* emit(\"world\");\n})\n  .resume(\"emit\", (msg) =\u003e emit(`\"${msg}\"`))\n  .resume(\"emit\", console.log);\n```\n\nWhen you run `program.runSync()`, the output will be:\n\n```text\n\"hello\"\n\"world\"\n```\n\n### Default handlers\n\nHaving to explicitly handle every effect can become tedious, especially for common effects like `println` or `random`. To address this, tinyeffect allows you to define default handlers that are automatically used when no specific handler is provided:\n\n```typescript\nconst println = effect\u003cunknown[], void\u003e()(\"println\", {\n  defaultHandler: ({ resume }, ...args) =\u003e {\n    console.log(...args);\n    resume();\n  },\n});\n\nconst program = effected(function* () {\n  yield* println(\"Hello, world!\");\n});\nprogram.runSync(); // No compile-time or runtime error\n// Hello, world!\n```\n\nNote the syntax difference when defining effects with default handlers. Instead of using `effect(name, options)\u003cParameters, ReturnType\u003e`, we use `effect\u003cParameters, ReturnType, TerminatedType = never\u003e()(name, options)`—do not forget the empty parentheses after `effect`. This difference exists because the type arguments are needed to accurately infer the handler’s type.\n\nFor better type clarity, you can use the `Effect` type and `EffectFactory` helper:\n\n```typescript\nimport { type Default, type Effect, type EffectFactory, effect } from \"tinyeffect\";\n\ntype Println = Default\u003cEffect\u003c\"println\", unknown[], void\u003e\u003e;\nconst println: EffectFactory\u003cPrintln\u003e = effect(\"println\", {\n  defaultHandler: ({ resume }, ...args) =\u003e {\n    console.log(...args);\n    resume();\n  },\n});\n```\n\nDefault handlers don’t prevent you from explicitly handling these effects. When you provide your own handler, it takes precedence over the default:\n\n```typescript\nconst program = effected(function* () {\n  yield* println(\"Hello, world!\");\n}).resume(\"println\", () =\u003e {\n  console.log(\"This will be logged instead of the default handler\");\n});\n\nprogram.runSync();\n// This will be logged instead of the default handler\n```\n\nJust like regular handlers, default handlers can also be generator functions or return effected programs, letting you handle effects using other effects.\n\nThe `dependency` helper also supports default handlers. You can provide a factory function as the second argument, which returns a default value when no explicit provider is given:\n\n```typescript\nconst askCurrentUser = dependency\u003cUser | null\u003e()(\"currentUser\", () =\u003e ({\n  id: 1,\n  name: \"Charlie\",\n  role: \"admin\",\n}));\n```\n\nBesides, it is worth noting that the `Default` type signature may appear more complex than expected:\n\n```typescript\ntype Default\u003cE extends Effect, T = never, F extends Effect = never\u003e = ...\n```\n\nHere, `E` represents the effect type, `T` is the terminate type (used if `terminate` is called in the default handler), and `F` represents any additional effects that might be performed by the default handler (when using a generator function or returning another effected program).\n\n\u003e [!NOTE]\n\u003e\n\u003e Default handlers technically support `terminate`, but this is not recommended since it always terminates the entire effected program (the `Effected` instance) with a specific value, which is usually unexpected behavior. It’s better to use `resume` in default handlers to allow the program to continue executing. If you want to terminate the program, it’s preferable to explicitly handle the effect with `.terminate()` rather than relying on a default handler.\n\n### Chaining effected programs with `.andThen()` and `.tap()`\n\nSometimes, you may want to transform a program’s return value, or chain another effected program after the current one. Let’s revisit the `safeDivide` example:\n\n```typescript\ntype Raise = Unresumable\u003cEffect\u003c\"raise\", [error: unknown], never\u003e\u003e;\nconst raise: EffectFactory\u003cRaise\u003e = effect(\"raise\", { resumable: false });\n\nconst safeDivide = (a: number, b: number): Effected\u003cRaise, number\u003e =\u003e\n  effected(function* () {\n    if (b === 0) return yield* raise(\"Division by zero\");\n    return a / b;\n  });\n```\n\nNow, suppose we have a type `Option` to represent a value that may or may not exist:\n\n```typescript\ntype Option\u003cT\u003e = { kind: \"some\"; value: T } | { kind: \"none\" };\n\nconst some = \u003cT\u003e(value: T): Option\u003cT\u003e =\u003e ({ kind: \"some\", value });\nconst none: Option\u003cnever\u003e = { kind: \"none\" };\n```\n\nWe want to transform the return value of `safeDivide` into an `Option` type: if `safeDivide` returns a value normally, we return `some(value)`, otherwise, we return `none` (if the raise effect is triggered). This can be achieved with the `.andThen()` method:\n\n```typescript\nconst safeDivide2 = (a: number, b: number): Effected\u003cnever, Option\u003cnumber\u003e\u003e =\u003e\n  safeDivide(a, b)\n    .andThen((value) =\u003e some(value))\n    .terminate(\"raise\", () =\u003e none);\n```\n\nNow, running `safeDivide2(1, 0).runSync()` will return `none`, while `safeDivide2(1, 2).runSync()` will return `some(0.5)`.\n\nSimilar to most other methods in tinyeffect, `.andThen()` can also be used with a generator function to chain another effected program, which can be incredibly useful in many scenarios. We’ll see many more examples of this in the following sections.\n\nBesides `.andThen(handler)`, the `.tap(handler)` method offers a useful alternative when you want to execute side effects without altering the return value. Unlike `.andThen()`, `.tap()` ignores the return value of the handler function, ensuring the original value is preserved. This makes it ideal for operations like logging, where the action doesn’t modify the main data flow.\n\nFor instance, you can use `.tap()` to simulate a `defer` effect similar to Go’s `defer` statement:\n\n```typescript\ntype Defer = Effect\u003c\"defer\", [fn: () =\u003e void], void\u003e;\nconst defer: EffectFactory\u003cDefer\u003e = effect(\"defer\");\n\nconst deferHandler = defineHandlerFor\u003cDefer\u003e().with((self) =\u003e {\n  const deferredActions: Array\u003c() =\u003e void\u003e = [];\n\n  return self\n    .resume(\"defer\", (fn) =\u003e {\n      deferredActions.push(fn);\n    })\n    .tap(() =\u003e {\n      deferredActions.forEach((fn) =\u003e fn());\n    });\n});\n\nconst program = effected(function* () {\n  yield* defer(() =\u003e console.log(\"Deferred action\"));\n  console.log(\"Normal action\");\n}).with(deferHandler);\n```\n\nWhen you run `program.runSync()`, you’ll see the following output:\n\n```text\nNormal action\nDeferred action\n```\n\n### Handling multiple effects in one handler\n\nImagine we have the following setup:\n\n```typescript\ntype Logging =\n  | Effect\u003c\"logging:log\", unknown[], void\u003e\n  | Effect\u003c\"logging:warn\", unknown[], void\u003e\n  | Effect\u003c\"logging:error\", unknown[], void\u003e;\nconst logger = {\n  log: effect(\"logging:log\")\u003cunknown[], void\u003e,\n  warn: effect(\"logging:warn\")\u003cunknown[], void\u003e,\n  error: effect(\"logging:error\")\u003cunknown[], void\u003e,\n};\n\ntype ReadFile = Effect\u003c\"readFile\", [path: string], string\u003e;\nconst readFile: EffectFactory\u003cReadFile\u003e = effect(\"readFile\");\n\ninterface Settings {\n  /* ... */\n}\n\nconst defaultSettings: Settings = {\n  /* ... */\n};\n\nconst readSettings = (path: string): Effected\u003cLogging | ReadFile, Settings\u003e =\u003e\n  effected(function* () {\n    const content = yield* readFile(path);\n    try {\n      const settings = JSON.parse(content);\n      yield* logger.log(\"Settings loaded\");\n      return settings;\n    } catch (e) {\n      yield* logger.error(\"Failed to parse settings file:\", e);\n      return defaultSettings;\n    }\n  });\n```\n\nThe `readSettings` function reads a JSON file, parses it, and returns the settings. If parsing fails, it logs an error and returns the default settings.\n\nIf you want to skip logging effects altogether, handling each one individually can be tedious. Instead of specifying an effect name for each `.handle()` method, you can use a type guard function to handle multiple effects at once. For example:\n\n```typescript\nconst readSettingsWithoutLogging = (path: string) =\u003e\n  readSettings(path).resume(\n    (name): name is Logging[\"name\"] =\u003e name.startsWith(\"logging:\"),\n    () =\u003e {\n      // Omit logging\n    },\n  );\n```\n\nHovering over `readSettingsWithoutLogging` will show its type signature:\n\n```typescript\nconst readSettingsWithoutLogging: (path: string) =\u003e Effected\u003cReadFile, Settings\u003e;\n```\n\nAnother useful case for this approach is wrapping all error effects in a `Result` type, similar to `Result` in Rust or `Either` in Haskell. Here’s how to define a Rust-like `Result` type in TypeScript:\n\n```typescript\ntype Result\u003cT, E\u003e = { kind: \"ok\"; value: T } | { kind: \"err\"; error: E };\n\nconst ok = \u003cT\u003e(value: T): Result\u003cT, never\u003e =\u003e ({ kind: \"ok\", value });\nconst err = \u003cE\u003e(error: E): Result\u003cnever, E\u003e =\u003e ({ kind: \"err\", error });\n```\n\nAssume you have an effected program that may throw different types of errors:\n\n```typescript\ntype TypeError = Effect.Error\u003c\"type\"\u003e;\nconst typeError: EffectFactory\u003cTypeError\u003e = error(\"type\");\ntype RangeError = Effect.Error\u003c\"range\"\u003e;\nconst rangeError: EffectFactory\u003cRangeError\u003e = error(\"range\");\n\ntype Log = Effect\u003c\"println\", unknown[], void\u003e;\nconst log: EffectFactory\u003cLog\u003e = effect(\"println\");\n\nconst range = (start: number, stop: number): Effected\u003cTypeError | RangeError | Log, number[]\u003e =\u003e\n  effected(function* () {\n    if (start \u003e= stop) return yield* rangeError(\"Start must be less than stop\");\n    if (!Number.isInteger(start) || !Number.isInteger(stop))\n      return yield* typeError(\"Start and stop must be integers\");\n    yield* log(`Generating range from ${start} to ${stop}`);\n    return Array.from({ length: stop - start }, (_, i) =\u003e start + i);\n  });\n```\n\nInstead of using individual error effects, you can convert them into a `Result` type. A helper function makes this transformation easy:\n\n```typescript\nconst handleErrorAsResult = \u003cR, E extends Effect, ErrorName extends string\u003e(\n  self: Effected\u003cEffect.Error\u003cErrorName\u003e | E, R\u003e,\n): Effected\u003cE, Result\u003cR, { error: ErrorName; message?: string }\u003e\u003e =\u003e {\n  const isErrorEffect = (name: string | symbol): name is `error:${ErrorName}` =\u003e {\n    if (typeof name === \"symbol\") return false;\n    return name.startsWith(\"error:\");\n  };\n\n  return self\n    .andThen((value) =\u003e ok(value))\n    .handle(isErrorEffect, (({ effect, terminate }: any, message: any) =\u003e {\n      terminate(err({ error: effect.name.slice(\"error:\".length), message }));\n    }) as never) as Effected\u003cE, Result\u003cR, { error: ErrorName; message?: string }\u003e\u003e;\n};\n```\n\nOne detail we haven’t mentioned earlier is that, aside from `resume` and `terminate`, the object passed as the first argument to the `.handle()` method also includes the effect object itself, giving you direct access to the effect’s name and payloads. In the `handleErrorAsResult` function, we use this feature to extract the error name from the effect name.\n\nThen simply wrap the `range` function with `handleErrorAsResult`:\n\n```typescript\nconst range2 = (start: number, stop: number) =\u003e handleErrorAsResult(range(start, stop));\n```\n\nNow, when you hover over the `range2` function, you’ll see this type signature:\n\n```typescript\nconst range2: (\n  start: number,\n  stop: number,\n) =\u003e Effected\u003cLog, Result\u003cnumber[], { error: \"type\" | \"range\"; message?: string }\u003e\u003e;\n```\n\nThe `handleErrorAsResult` helper function shown above is mainly for illustration — tinyeffect actually provides a built-in `.catchAll()` method on `Effected` instances, which lets you handle all error effects at once. Here’s how it works:\n\n```typescript\nconst range3 = (start: number, stop: number) =\u003e\n  range(start, stop)\n    .andThen((value) =\u003e ok(value))\n    .catchAll((error, message) =\u003e err({ error, message }));\n```\n\nThe `.catchAll()` method takes a function that receives the error effect name and message, and returns a new value. `range3` behaves the same as `range2`, with an identical type signature.\n\n### Handling error effects\n\nWith side effects, including errors, now handled in a unified way, you may wonder where `try-catch` fits in. The answer is simple: it’s no longer needed. Errors are just effects, so you can handle specific ones with `.catch()` and let others bubble up to higher-level handlers.\n\nFor example, suppose your program accepts a JSON string to define settings. You use `JSON.parse` to parse it, but if the JSON is invalid, instead of throwing an error, you want to print a warning and fall back on a default setting. Here’s how to do it:\n\n```typescript\ntype SyntaxError = Effect.Error\u003c\"syntax\"\u003e;\nconst syntaxError: EffectFactory\u003cSyntaxError\u003e = error(\"syntax\");\n// For other unexpected errors, we use an unresumable effect \"raise\" to terminate the program\ntype Raise = Unresumable\u003cEffect\u003c\"raise\", [error: unknown], never\u003e\u003e;\nconst raise: EffectFactory\u003cRaise\u003e = effect(\"raise\", { resumable: false });\n\nconst parseJSON = \u003cT\u003e(json: string): Effected\u003cSyntaxError | Raise, T\u003e =\u003e\n  effected(function* () {\n    try {\n      return JSON.parse(json);\n    } catch (e) {\n      if (e instanceof SyntaxError) return yield* syntaxError(e.message);\n      return yield* raise(e);\n    }\n  });\n\ninterface Settings {\n  /* ... */\n}\n\nconst defaultSettings: Settings = {\n  /* ... */\n};\n\nconst readSettings = (json: string) =\u003e\n  effected(function* () {\n    const settings = yield* parseJSON\u003cSettings\u003e(json).catch(\"syntax\", (message) =\u003e {\n      console.error(`Invalid JSON: ${message}`);\n      return defaultSettings;\n    });\n    /* ... */\n  });\n```\n\nAs shown in the previous section, you can also use `.catchAll()` to catch all error effects at once, which is useful if you want a unified response to all errors. For instance:\n\n```typescript\nconst tolerantRange = (start: number, stop: number): Effected\u003cLog, number[]\u003e =\u003e\n  range(start, stop).catchAll((error, message) =\u003e {\n    console.warn(`Error(${error}): ${message || \"\"}`);\n    return [];\n  });\n```\n\nRunning `tolerantRange(4, 1).resume(\"log\", console.log).runSync()` will output `[]`, with a warning message printed to the console:\n\n```text\nError(range): Start must be less than stop\n```\n\nIf you prefer some errors to raise exceptions instead of handling them within your effects system, you can use the `.catchAndThrow(error, message?)` method:\n\n```typescript\n// Throws \"type\" error effect as an exception with its original message\nconst range2 = (start: number, stop: number) =\u003e range(start, stop).catchAndThrow(\"type\");\n\n// Throws \"type\" error effect with a custom message\nconst range3 = (start: number, stop: number) =\u003e\n  range(start, stop).catchAndThrow(\"type\", \"Invalid start or stop value\");\n\n// Throws \"range\" error effect with a customized message based on the error\nconst range4 = (start: number, stop: number) =\u003e\n  range(start, stop).catchAndThrow(\"range\", (message) =\u003e `Invalid range: ${message}`);\n```\n\nFor example, running `range2(1.5, 2).catch(\"range\", () =\u003e {}).resume(\"log\", console.log).runSync()` will throw an exception with the message “Start and stop must be integers”.\n\nTo throw all error effects as exceptions, you can use `.catchAllAndThrow(message?)`:\n\n```typescript\nconst range2 = (start: number, stop: number) =\u003e range(start, stop).catchAllAndThrow();\n\nconst range3 = (start: number, stop: number) =\u003e\n  range(start, stop).catchAllAndThrow(\"An error occurred while generating the range\");\n\nconst range4 = (start: number, stop: number) =\u003e\n  range(start, stop).catchAllAndThrow((error, message) =\u003e `Error(${error}): ${message}`);\n```\n\n### Abstracting/combining handlers\n\nNot all effects are totally independent from each other; sometimes, you may want to “group” effects that are closely related. A pair of getters and setters for global state is a good example (from the [Koka documentation](https://koka-lang.github.io/koka/doc/book.html#sec-return)):\n\n```typescript\nconst getState = \u003cT\u003e() =\u003e effect(\"getState\")\u003c[], T\u003e();\nconst setState = \u003cT\u003e(value: T) =\u003e effect(\"setState\")\u003c[value: T], void\u003e();\n```\n\nTo group these together, we could define them as:\n\n```typescript\ntype State\u003cT\u003e = Effect\u003c\"state.get\", [], T\u003e | Effect\u003c\"state.set\", [value: T], void\u003e;\nconst state = {\n  get: \u003cT\u003e(): Effected\u003cState\u003cT\u003e, T\u003e =\u003e effect(\"state.get\")\u003c[], T\u003e(),\n  set: \u003cT\u003e(value: T): Effected\u003cState\u003cT\u003e, void\u003e =\u003e effect(\"state.set\")\u003c[value: T], void\u003e(value),\n};\n```\n\nUsing these state effects looks like this:\n\n```typescript\nconst sumDown = (sum: number = 0): Effected\u003cState\u003cnumber\u003e, number\u003e =\u003e\n  effected(function* () {\n    const n = yield* state.get\u003cnumber\u003e();\n    if (n \u003c= 0) return sum;\n    yield* state.set(n - 1);\n    return yield* sumDown(sum + n);\n  });\n\nlet n = 10;\nconst program = sumDown()\n  .resume(\"state.get\", () =\u003e n)\n  .resume(\"state.set\", (value) =\u003e {\n    n = value;\n  });\n```\n\nRunning `program.runSync()` returns `55`, which is the sum of the first `10` natural numbers.\n\nJust grouping the effects together is easy, but we still have to handle them one by one. To make it easier, we can create a helper function to abstract out the handlers:\n\n```javascript\nconst stateHandler =\n  ({ get, set }) =\u003e\n  (self) =\u003e\n    self.resume(\"state.get\", get).resume(\"state.set\", set);\n\nstateHandler({ get: () =\u003e n, set: (x) =\u003e (n = x) })(sumDown()).runSync();\n```\n\nFor simplicity, this example is in JavaScript, focusing on the concept. Here, `stateHandler` is a higher-order function that accepts handler methods and returns a function to apply them to an effected program.\n\nThe main challenge lies in defining the correct type signature for the `stateHandler` function. Due to certain limitations in TypeScript, it can be tricky to annotate `stateHandler` in a way that reliably covers all edge cases.\n\nFortunately, tinyeffect performs some type-level magic and provides a helper function, `defineHandlerFor`, which you can use with the `.with()` method to define handlers for one or more effects. Here’s how it works:\n\n```typescript\nimport { defineHandlerFor } from \"tinyeffect\";\n\nconst stateHandler = \u003cT\u003e({ get, set }: { get: () =\u003e T; set: (x: T) =\u003e void }) =\u003e\n  defineHandlerFor\u003cState\u003cT\u003e\u003e().with((self) =\u003e\n    self.resume(\"state.get\", get).resume(\"state.set\", set),\n  );\n\nlet n = 10;\nconst program = sumDown().with(stateHandler({ get: () =\u003e n, set: (x) =\u003e (n = x) }));\n```\n\n`defineHandlerFor\u003c...\u003e().with(...)` simply returns your function at runtime, and the `.with(handler)` method applies the handler to the effected program. This keeps the runtime logic identical to previous implementations, while TypeScript infers the correct type signature for the handler.\n\nHovering over `stateHandler` shows its type signature:\n\n```typescript\nconst stateHandler: \u003cT\u003e({\n  get,\n  set,\n}: {\n  get: () =\u003e T;\n  set: (x: T) =\u003e void;\n}) =\u003e \u003cR\u003e(self: EffectedDraft\u003cState\u003cT\u003e, State\u003cT\u003e, R\u003e) =\u003e EffectedDraft\u003cState\u003cT\u003e, never, R\u003e;\n```\n\nThe `EffectedDraft` type is used internally by tinyeffect to achieve precise type inference.\n\nLet’s revisit the `safeDivide` example we defined earlier:\n\n```typescript\ntype Raise = Unresumable\u003cEffect\u003c\"raise\", [error: unknown], never\u003e\u003e;\nconst raise: EffectFactory\u003cRaise\u003e = effect(\"raise\", { resumable: false });\n\nconst safeDivide = (a: number, b: number): Effected\u003cRaise, number\u003e =\u003e\n  effected(function* () {\n    if (b === 0) return yield* raise(\"Division by zero\");\n    return a / b;\n  });\n```\n\nand the `Option` type:\n\n```typescript\ntype Option\u003cT\u003e = { kind: \"some\"; value: T } | { kind: \"none\" };\n\nconst some = \u003cT\u003e(value: T): Option\u003cT\u003e =\u003e ({ kind: \"some\", value });\nconst none: Option\u003cnever\u003e = { kind: \"none\" };\n```\n\nIn previous examples, we used `.andThen()` and `.terminate()` to transform the return value of `safeDivide` to an `Option` type. Now, we can abstract this logic into a reusable handler:\n\n```typescript\nconst raiseOption = defineHandlerFor\u003cRaise\u003e().with((self) =\u003e\n  self.andThen((value) =\u003e some(value)).terminate(\"raise\", () =\u003e none),\n);\n\nconst safeDivide2 = (a: number, b: number) =\u003e safeDivide(a, b).with(raiseOption);\n```\n\nHovering over `safeDivide2` reveals this type signature:\n\n```typescript\nconst safeDivide2: (a: number, b: number) =\u003e Effected\u003cnever, Option\u003cnumber\u003e\u003e;\n```\n\nFor more complex cases where `defineHandlerFor` isn’t sufficient, you can still define a function that takes an effected program and returns another one as a custom “handler” function, then pass it to the `.with(handler)` method. A good example of this is the `handleErrorAsResult` function we defined earlier:\n\n```typescript\n// Both definitions below are equivalent\nconst range = (start: number, stop: number) =\u003e range(start, stop).with(handleErrorAsResult);\nconst range = (start: number, stop: number) =\u003e handleErrorAsResult(range(start, stop));\n```\n\n### Parallel execution with `Effected.all`\n\nWhen working with asynchronous effects, you frequently need to combine multiple operations. While generator syntax excels at expressing sequential code, it doesn’t provide a native way to run effects in parallel using `yield*`. To address this, tinyeffect offers two complementary methods for handling multiple effected programs:\n\n- `Effected.all()`: Executes effected programs in parallel (concurrently)\n- `Effected.allSeq()`: Executes effected programs sequentially (one after another), equivalent to running them individually with `yield*`.\n\nIt’s worth noting that when all effected programs are synchronous, `Effected.all()` and `Effected.allSeq()` produce identical results. The difference becomes significant when dealing with time-consuming operations:\n\n```typescript\nconst fetchUserData = (userId: number) =\u003e\n  effected(function* () {\n    yield* log(`Fetching user ${userId}`);\n    const data = yield* httpGet(`/api/users/${userId}`);\n    return data;\n  });\n\n// Sequential execution - total time is the sum of all operations\nconst sequentialFetch = Effected.allSeq([fetchUserData(1), fetchUserData(2), fetchUserData(3)]); // Takes ~300ms if each fetch takes ~100ms\n\n// Parallel execution - total time is close to the slowest operation\nconst parallelFetch = Effected.all([fetchUserData(1), fetchUserData(2), fetchUserData(3)]); // Takes ~100ms because all fetches run concurrently\n```\n\nBoth methods accept either an iterable of effected programs or an object with named effected programs:\n\n```typescript\n// Iterable syntax - results will be an array\nconst users = await Effected.all([fetchUser(1), fetchUser(2), fetchUser(3)]).runAsync();\n// users: [User, User, User]\n\n// Object syntax - results maintain property names\nconst userData = await Effected.all({\n  user: fetchUser(userId),\n  posts: fetchUserPosts(userId),\n  settings: fetchUserSettings(userId),\n}).runAsync();\n// userData: { user: User, posts: Post[], settings: Settings }\n```\n\nYou can mix synchronous and asynchronous effects, and `Effected.all` will handle them efficiently:\n\n```typescript\nconst compute = effect(\"compute\")\u003c[label: string, delay: number], number\u003e;\nconst calculate = effect(\"calculate\")\u003c[a: number, b: number], number\u003e;\n\n// Create a mix of sync and async tasks\nconst program = effected(function* () {\n  const results = yield* Effected.all([\n    // Sync task\n    calculate(10, 5),\n    // Fast async task\n    compute(\"fast task\", 50),\n    // Slow async task\n    compute(\"slow task\", 150),\n  ]);\n  console.log(\"Results:\", results);\n})\n  .resume(\"calculate\", (a, b) =\u003e a + b)\n  .handle(\"compute\", ({ resume }, label, delay) =\u003e {\n    console.log(`Starting ${label}`);\n    setTimeout(() =\u003e {\n      console.log(`Completed ${label}`);\n      resume(delay);\n    }, delay);\n  });\n\n// Results will be [15, 50, 150]\n// Total execution time will be ~150ms (the slowest task)\n```\n\nWhen should you choose sequential execution with `Effected.allSeq`? Consider using it when:\n\n1. Operations must happen in a specific order.\n2. Later operations depend on earlier ones.\n3. You need to limit resource usage by preventing concurrent operations.\n\n```typescript\n// Use sequential execution when operations must happen in order\nconst processData = Effected.allSeq([\n  setupDatabase(),\n  migrateSchema(),\n  importData(),\n  validateData(),\n]);\n\n// The equivalent generator syntax\nconst processData = effected(function* () {\n  yield* setupDatabase();\n  yield* migrateSchema();\n  yield* importData();\n  yield* validateData();\n});\n```\n\nFor most other cases, it is recommended to use `Effected.all` over `Effected.allSeq`, since it is more efficient and easier to read.\n\n### Effects without generators (Pipeline syntax)\n\nThe fundamental logic of tinyeffect is _not_ dependent on generators. An effected program (represented as an `Effected` instance) is essentially an iterable object that implements a `[Symbol.iterator](): Iterator\u003cEffect\u003e` method.\n\nAlthough using the `effected` helper function with generators allows you to write more imperative-style code using `yield*` to manage effects, this is not the only approach. tinyeffect offers an alternative pipeline-style API for transforming and combining effected programs. At the heart of this API is the `.andThen()` method, which serves as the primary way to transform and chain effected programs.\n\nWhile we've covered `.andThen()` in previous sections, we haven’t yet explored how it can be used in a more functional, pipeline-style manner. The `.andThen(handler)` method is quite versatile and can be used in several ways:\n\nWhile we have covered `.andThen()` in previous sections, we haven’t yet explored how it can be used in a more functional, pipeline-style manner. Actually, `.andThen(handler)` is quite versatile and can be used in a variety of ways:\n\n- Transform a result using a pure function.\n- Chain another effected program.\n- Work with generators that yield effects.\n\nLet’s rewrite the `createUser` example using the pipeline syntax:\n\n```typescript\nconst createUser = (user: Omit\u003cUser, \"id\"\u003e) =\u003e\n  requiresAdmin()\n    .andThen(() =\u003e\n      executeSQL(\"INSERT INTO users (name) VALUES (?)\", user.name)\n        .andThen((id) =\u003e ({ id, ...user } as User)),\n    )\n    .tap((savedUser) =\u003e println(\"User created:\", savedUser));\n```\n\nA helpful way to understand this code is to think of `Effected` as a container for a delayed computation (or _monad_, if you come from a functional programming background). The `Effected` instance itself doesn’t perform any computation; it only represents a sequence of effects that will be executed when you call `.runSync()` or `.runAsync()`.\n\nYou can compare `Effected` with `Promise` in JavaScript. Just like `Promise.prototype.then(handler)` allows you to chain multiple promises together, `Effected.prototype.andThen(handler)` allows you to chain multiple effected programs together. If a handler returns a generator or another effected program, it will be automatically flattened, similar to how `Promise.prototype.then()` works in JavaScript.\n\nTo create effects without generators, tinyeffect provides two foundational methods. `Effected.of(value)` creates an effected program that immediately resolves to the given value without performing any effects — similar to `Promise.resolve(value)`. `Effected.from(() =\u003e value)` allows you to execute a function lazily when the program is run. These are useful as starting points for pipeline-style code:\n\n```typescript\n// Create an effected program that resolves to \"Hello, world!\"\nconst program1 = Effected.of(\"Hello, world!\")\n  .tap((message) =\u003e println(message))\n  .andThen((message) =\u003e message.toUpperCase());\n\n// Create an effected program that executes the function when run\nconst program2 = Effected.from(() =\u003e {\n  console.log(\"Computing value...\");\n  return Math.random() * 100;\n}).andThen((value) =\u003e println(`Random value: ${value}`));\n```\n\nWhen you need more explicit control, tinyeffect offers `.map()` and `.flatMap()`. The `.map()` method transforms a result without introducing new effects, while `.flatMap()` expects the handler to return another effected program:\n\n```typescript\nconst createUser = (user: Omit\u003cUser, \"id\"\u003e) =\u003e\n  requiresAdmin()\n    .flatMap(() =\u003e\n      executeSQL(\"INSERT INTO users (name) VALUES (?)\", user.name)\n        .map((id) =\u003e ({ id, ...user } as User)),\n    )\n    .tap((savedUser) =\u003e println(\"User created:\", savedUser));\n```\n\nFor the common case of replacing a result with a constant value, use the `.as(value)` method as a shorthand for `.map(() =\u003e value)`:\n\n```typescript\nEffected.of(42).as(\"Hello, world!\").runSync(); // =\u003e \"Hello, world!\"\n// You can also use `.asVoid()` as a shortcut for `.as(undefined)`\nEffected.of(42).asVoid().runSync(); // =\u003e undefined\n```\n\nIn most cases, `.andThen()` is recommended over `.map()` and `.flatMap()` for its versatility and readability. A myth is that `.flatMap()/.map()` may provide better performance than `.andThen()` since they do not need to check if the handler returns a generator or another effected program. However, in practice, the performance difference is negligible, so it’s better to use `.andThen()` directly for simplicity and consistency.\n\nFor convenience, tinyeffect also provides a `.zip()` method to combine two effected programs sequentially (unlike `Effected.all()` which runs them in parallel). The `.zip()` method either returns their results as a tuple `[A, B]` or applies a mapper function to transform the combined results:\n\n```typescript\n// Get user and settings\nconst getUserName = askCurrentUser().map((user) =\u003e user?.name || \"Guest\");\nconst askTheme = dependency(\"theme\")\u003c\"light\" | \"dark\"\u003e;\n\n// Combine them with zip to create a welcome message with theme info\nconst welcomeMessage = getUserName\n  .zip(askTheme()) // Returns a tuple [username, theme]\n  .map(([username, theme]) =\u003e `Welcome ${username}! Using ${theme} theme.`);\n\n// You can also provide a mapper function directly to zip:\nconst welcomeMessage = getUserName.zip(\n  askTheme(),\n  (username, theme) =\u003e `Welcome ${username}! Using ${theme} theme.`,\n);\n\n// Both approaches produce the same result when run\n```\n\nJust like `.andThen()`, `.zip()` also allows you to use a generator function or another effected program as the handler, which will be flattened automatically.\n\nWhen built-in methods aren’t sufficient for your needs, you can create custom transformers and chain them with existing effected programs using the `.pipe(...fs)` method. This allows you to apply multiple transformations in a clean, functional style (inspired by [Effect](https://effect.website/docs/getting-started/building-pipelines/#the-pipe-method)):\n\n```typescript\ntype Sleep = Default\u003cEffect\u003c\"sleep\", [ms: number], void\u003e\u003e;\nconst sleep: EffectFactory\u003cSleep\u003e = effect(\"sleep\", {\n  defaultHandler: ({ resume }, ms) =\u003e {\n    setTimeout(resume, ms);\n  },\n});\n\nconst delay =\n  (ms: number) =\u003e\n  \u003cE extends Effect, R\u003e(self: Effected\u003cE, R\u003e): Effected\u003cE | Sleep, R\u003e =\u003e\n    sleep(ms).andThen(() =\u003e self);\n\nconst withLog =\n  (message: string) =\u003e\n  \u003cE extends Effect, R\u003e(self: Effected\u003cE, R\u003e): Effected\u003cE, R\u003e =\u003e\n    self.tap((value) =\u003e {\n      console.log(`${message}: ${String(value)}`);\n    });\n\n// Add a delay of 1000ms to the effected program\nconsole.log(await Effected.of(42).pipe(delay(1000)).runAsync());\n\n// You can use multiple transformers in `.pipe()`\nawait Effected.of(42).pipe(delay(1000), withLog(\"Result\")).runAsync();\n// Result: 42\n```\n\n### Pipeline Syntax V.S. Generator Syntax\n\nBoth pipeline syntax and generator syntax are valid approaches for working with effected programs in tinyeffect. Each approach has distinct advantages:\n\n**Generator Syntax:**\n\n- More familiar to developers used to imperative programming.\n- Natural handling of conditionals and loops.\n- Simpler debugging with sequential steps.\n\n**Pipeline Syntax:**\n\n- More functional approach with method chaining.\n- Reduces nesting for simple transformations.\n- Offers better performance in some cases.\n\nWhile pipeline syntax offers better performance for simple transformations, in reality such advantages are often negligible since IO-bound effects (like HTTP requests or file operations) usually dominate the execution time. Therefore, the choice between the two should primarily be based on readability and maintainability.\n\nBelow are several examples where pipeline syntax might seem more straightforward:\n\n```typescript\n// Generator syntax\nconst getUserPosts = (userId: number) =\u003e\n  effected(function* () {\n    const user = yield* fetchUser(userId);\n    if (!user) return null;\n    return yield* fetchPosts(user.id);\n  });\n\n// Pipeline syntax\nconst getUserPosts = (userId: number) =\u003e\n  fetchUser(userId).andThen((user) =\u003e {\n    if (!user) return null;\n    return fetchPosts(user.id);\n  });\n```\n\nAnother example for error handling:\n\n```typescript\n// Generator syntax\nconst processFile = (path: string) =\u003e\n  effected(function* () {\n    const content = yield* readFile(path);\n    return yield* parseContent(content);\n  }).catchAll(function* (error, message) {\n    yield* logger.error(`[${error}Error] Error processing ${path}:`, message);\n    return null;\n  });\n\n// Pipeline syntax\nconst processFile = (path: string) =\u003e\n  readFile(path)\n    .andThen((content) =\u003e parseContent(content))\n    .catchAll((error, message) =\u003e\n      logger.error(`[${error}Error] Error processing ${path}:`, message).as(null),\n    );\n```\n\nHowever, when dealing with complex control flow, generator syntax might be more readable:\n\n```typescript\n// Generator syntax\nconst submitOrder = (order: Order) =\u003e\n  effected(function* () {\n    const [config, user] = yield* Effected.all([askConfig(), askCurrentUser()]);\n    yield* validateOrder(order, user);\n    const result = yield* saveOrder(order, config.apiUrl);\n    yield* sendNotification(user.email, \"Order submitted\");\n    return result;\n  });\n\n// Pipeline syntax\nconst submitOrder = (order: Order) =\u003e\n  Effected.all([askConfig(), askCurrentUser()]).andThen(([config, user]) =\u003e\n    validateOrder(order, user).andThen(() =\u003e\n      saveOrder(order, config.apiUrl).tap(() =\u003e\n        sendNotification(user.email, \"Order submitted\").asVoid(),\n      ),\n    ),\n  );\n```\n\nWhile the pipeline syntax shown above is more compact, it may not be as readable as the generator syntax for most developers since it involves more nesting.\n\nBoth generator syntax and pipeline syntax are fully supported in tinyeffect — choose whichever approach makes your code most readable and maintainable for you and your team. The best choice often depends on the specific task and your team’s preferences.\n\n### Example: Build a configurable logging system with effects\n\nLet’s walk through a practical example of using algebraic effects to build a flexible logging system similar to what you might use in a real application. We aim to achieve the following goals:\n\n- Support multiple logging levels (debug, info, warn, error).\n- Allow setting minimum logging levels for different parts of the application.\n- Enable logging to different outputs (console, file, etc.).\n- Provide a way to customize the logging format.\n\nIn the following example, we’ll achieve this by defining a set of effects for logging, creating default effect handlers that log to the console, and defining several helper functions to manage logging levels and outputs. All of this is implemented in ~60 lines of code.\n\n**Step 1: Define a dependency for logger**\n\nWe’ll start by defining a dependency for the logger, which will be used to redirect log messages to different outputs.\n\n```typescript\nexport interface Logger {\n  debug: (...args: unknown[]) =\u003e void | Promise\u003cvoid\u003e;\n  info: (...args: unknown[]) =\u003e void | Promise\u003cvoid\u003e;\n  warn: (...args: unknown[]) =\u003e void | Promise\u003cvoid\u003e;\n  error: (...args: unknown[]) =\u003e void | Promise\u003cvoid\u003e;\n}\n\n// Define a dependency for injecting a logger\nexport type LoggerDependency = Default\u003cEffect.Dependency\u003c\"logger\", Logger\u003e\u003e;\n// Create dependency with console as default implementation\nexport const askLogger = dependency\u003cLogger\u003e()(\"logger\", () =\u003e console);\n```\n\nNote that we allow loggers to be asynchronous, so you can implement a logger that writes to a file or sends logs to a remote server. This is one advantage of algebraic effects: you don’t need to distinguish between synchronous and asynchronous effects, as they are all treated uniformly.\n\n**Step 2: Create effects for each log level**\n\nNext, we’ll define effects for each log level. These effects will be used to log messages at different levels.\n\n```typescript\nexport type Logging =\n  | Default\u003cEffect\u003c\"logging.debug\", unknown[], void\u003e, never, LoggerDependency\u003e\n  | Default\u003cEffect\u003c\"logging.info\", unknown[], void\u003e, never, LoggerDependency\u003e\n  | Default\u003cEffect\u003c\"logging.warn\", unknown[], void\u003e, never, LoggerDependency\u003e\n  | Default\u003cEffect\u003c\"logging.error\", unknown[], void\u003e, never, LoggerDependency\u003e;\n\nexport const logLevels = [\"debug\", \"info\", \"warn\", \"error\"] as const;\nexport type LogLevel = (typeof logLevels)[number];\n\n// A helper function to create a logging effect for each level\nconst logEffect = (level: LogLevel): EffectFactory\u003cLogging\u003e =\u003e\n  effect(`logging.${level}`, {\n    *defaultHandler({ resume }, ...args) {\n      const logger = yield* askLogger();\n      const result = logger[level](...args);\n      // Handle async loggers\n      if (result instanceof Promise) result.then(resume);\n      else resume();\n    },\n  });\n\n// Create effect functions for each log level\nexport const logDebug = logEffect(\"debug\");\nexport const logInfo = logEffect(\"info\");\nexport const logWarn = logEffect(\"warn\");\nexport const logError = logEffect(\"error\");\n```\n\nWe define a default effect handler that relies on the `Logger` dependency for each log level. This allows us to control which log levels are enabled at runtime.\n\n**Step 3: Define handlers for common logging features**\n\nWe’ll define several helper functions to manage logging levels and outputs. The `defineHandlerFor` helper will be used to create these helper functions.\n\nThe first helper is `withPrefix(prefixFactory: (level) =\u003e string)`, which adds a prefix to each log message based on the log level. This is useful for distinguishing between different log levels in the output.\n\n```typescript\nexport function withPrefix(prefixFactory: (level: LogLevel) =\u003e string) {\n  return defineHandlerFor\u003cLogging\u003e().with((self) =\u003e\n    self.handle(\n      (name): name is Logging[\"name\"] =\u003e typeof name === \"string\" \u0026\u0026 name.startsWith(\"logging.\"),\n      function* ({ effect, resume }): Generator\u003cLogging, void\u003e {\n        const prefix = prefixFactory(effect.name.slice(\"logging.\".length) as LogLevel);\n        // Insert prefix at the beginning of the payloads\n        effect.payloads.splice(0, 0, prefix);\n        yield effect; // Re-yield the effect with the modified payloads\n        resume();\n      },\n    ),\n  );\n}\n```\n\nThe next is `withMinimumLogLevel(level)`, which filters out log messages below a specified minimum level. This allows you to control the verbosity of the logs based on the current logging level.\n\n```typescript\nexport function withMinimumLogLevel(level: LogLevel | \"none\") {\n  return defineHandlerFor\u003cLogging\u003e().with((self) =\u003e {\n    const disabledLevels = new Set(\n      level === \"none\" ? logLevels : logLevels.slice(0, logLevels.indexOf(level)),\n    );\n    return self.handle(\n      (name): name is Logging[\"name\"] =\u003e\n        typeof name === \"string\" \u0026\u0026\n        name.startsWith(\"logging.\") \u0026\u0026\n        disabledLevels.has(name.slice(\"logging.\".length) as LogLevel),\n      function* ({ effect, resume }): Generator\u003cLogging, void\u003e {\n        // Change default handler of disabled log levels to resume immediately\n        effect.defaultHandler = ({ resume }) =\u003e resume();\n        yield effect; // Re-yield the effect with the modified default handler\n        resume();\n      },\n    );\n  });\n}\n```\n\n**Step 4: Use the logging system!**\n\nDone! We’ve already created a fully functional logging system. But now you might wonder how to use it in practice. Let’s start by creating a simple program that uses the logging system:\n\n```typescript\nconst program = effected(function* () {\n  yield* logDebug(\"Debug message\");\n  yield* logInfo(\"Info message\");\n  yield* logWarn(\"Warning!\");\n  yield* logError(\"Error occurred!\");\n});\n\nawait program.runAsync();\n```\n\nWe do not explicitly handle any logging effects, so the default handler will be used, which logs to the console. The output will look like this:\n\n```text\nDebug message\nInfo message\nWarning!\nError occurred!\n```\n\nBy default, all log levels are enabled, so all messages are printed. Now, let’s set the minimum log level to `warn` to disable debug and info messages:\n\n```typescript\nconst program = effected(function* () {\n  // ...\n}).pipe(withMinimumLogLevel(\"warn\"));\n```\n\nThe output will now only show the warning and error messages:\n\n```text\nWarning!\nError occurred!\n```\n\nFor now, it’s not easy to distinguish between different log levels. To add a prefix to each log message, we can use the `withPrefix` helper function:\n\n```typescript\nfunction logPrefix(level: LogLevel) {\n  const date = new Date();\n  const yyyy = date.getFullYear();\n  const MM = String(date.getMonth() + 1).padStart(2, \"0\");\n  const dd = String(date.getDate()).padStart(2, \"0\");\n  const HH = String(date.getHours()).padStart(2, \"0\");\n  const mm = String(date.getMinutes()).padStart(2, \"0\");\n  const ss = String(date.getSeconds()).padStart(2, \"0\");\n  const ms = String(date.getMilliseconds()).padEnd(3, \"0\");\n  const datePart = `[${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}.${ms}]`;\n  const levelPart = `[${level}]`;\n  return `${datePart} ${levelPart}`;\n}\n\nconst program = effected(function* () {\n  // ...\n}).pipe(withMinimumLogLevel(\"warn\"), withPrefix(logPrefix));\n```\n\nNow, the output will look like this:\n\n```text\n[2025-03-29 21:49:03.633] [warn] Warning!\n[2025-03-29 21:49:03.634] [error] Error occurred!\n```\n\nWhat if we want to log to a file instead of the console? We can create a custom logger that writes to a file and provide it as the `Logger` dependency. Here’s an example of how to do this:\n\n```typescript\nimport fs from \"node:fs/promises\";\nimport { show } from \"showify\";\n\nfunction fileLogger(path: string) {\n  return new Proxy({} as Logger, {\n    get() {\n      return async (...args: unknown[]) =\u003e {\n        const message = args\n          .map((arg) =\u003e (typeof arg === \"string\" ? arg : show(arg, { indent: 2 })))\n          .join(\" \");\n        await fs.appendFile(path, message + \"\\n\");\n      };\n    },\n  });\n}\n\nconst program = effected(function* () {\n  // ...\n})\n  .pipe(withMinimumLogLevel(\"warn\"), withPrefix(logPrefix))\n  .provide(\"logger\", fileLogger(\"log.txt\"));\n```\n\nIn this example, we use [showify](https://github.com/Snowflyt/showify) to format the log messages into human-readable strings. The `fileLogger` function accepts a file path and returns a logger object that writes log messages to that file. We use `node:fs/promises` to handle file operations asynchronously.\n\nNow, instead of logging to the console, all log messages will be written to the `log.txt` file. You can customize the logger to write to different outputs, such as a database or a remote server.\n\nYou can also create a helper function to combine multiple handlers together:\n\n```typescript\nfunction dualLogger(logger1: Logger, logger2: Logger) {\n  return new Proxy({} as Logger, {\n    get(_, prop, receiver) {\n      return (...args: unknown[]) =\u003e {\n        const result1 = Reflect.get(logger1, prop, receiver)(...args);\n        const result2 = Reflect.get(logger2, prop, receiver)(...args);\n        if (result1 instanceof Promise \u0026\u0026 result2 instanceof Promise)\n          return Promise.all([result1, result2]);\n        else if (result1 instanceof Promise) return result1;\n        else if (result2 instanceof Promise) return result2;\n      };\n    },\n  });\n}\n\nconst program = effected(function* () {\n  // ...\n})\n  .pipe(withMinimumLogLevel(\"warn\"), withPrefix(logPrefix))\n  .provide(\"logger\", dualLogger(console, fileLogger(\"log.txt\")));\n```\n\nNow, all log messages will be logged to both the console and the `log.txt` file.\n\nTo extend the logging system, you can create additional effects for other log levels, such as `trace`, `fatal`, or a `log` effect that defaults to `info` but can be configured to use any log level. In real applications, you can also redirect log messages to a worker thread and then handle them with a message queue, allowing you to log messages without blocking the main thread.\n\n## FAQ\n\n### What’s the relationship between tinyeffect and Effect?\n\nIt is a coincidence that the name “tinyeffect” is similar to [Effect](https://github.com/Effect-TS/effect), a TypeScript ecosystem inspired by the effect ecosystem in Scala. However, “effect” means different things in both libraries. In tinyeffect, “effect” refers to “algebraic effect”, while in Effect, it means “effectful computation”. Both libraries are independent and developed for different purposes.\n\nHowever, it is not surprising that the two libraries share some similarities, as they both aim to provide a way to handle side effects in a type-safe manner. The `Effect` type in Effect and the `Effected` type in tinyeffect are both monads that abstract effectful computations, and they share similarities in their API design, e.g., `Effect.map()` vs. `Effected.prototype.map()`, `Effect.flatMap()` vs. `Effected.prototype.flatMap()`, `Effect.andThen()` vs. `Effected.prototype.andThen()`, etc.\n\nWhile sharing some similarities, tinyeffect and Effect are fundamentally different in their design and implementation. tinyeffect is designed to be a lightweight library that focuses on providing a simple and intuitive API for handling side effects, while Effect is designed to be a full-fledged library that provides a comprehensive set of features for building effectful applications. Also, both libraries provide concurrency primitives, but Effect uses a fiber-based concurrency model, whereas tinyeffect uses a simple iterator-based model.\n","funding_links":[],"categories":["Libraries"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FSnowflyt%2Ftinyeffect","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FSnowflyt%2Ftinyeffect","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FSnowflyt%2Ftinyeffect/lists"}