{"id":19972610,"url":"https://github.com/jpb06/remix-effect-errors","last_synced_at":"2025-10-09T10:16:55.629Z","repository":{"id":236362852,"uuid":"792457692","full_name":"jpb06/remix-effect-errors","owner":"jpb06","description":"A showcase for effect errors reporting ","archived":false,"fork":false,"pushed_at":"2025-07-20T08:56:09.000Z","size":2527,"stargazers_count":2,"open_issues_count":10,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-20T10:25:16.562Z","etag":null,"topics":["effect-ts","error-reporting","panda-css","react","remix","source-maps"],"latest_commit_sha":null,"homepage":"https://remix-effect-errors.vercel.app/","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/jpb06.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-04-26T17:54:57.000Z","updated_at":"2025-07-13T15:47:11.000Z","dependencies_parsed_at":"2024-05-23T00:31:29.202Z","dependency_job_id":"8e392e3f-8ec1-43ff-a2ed-0de2c917280f","html_url":"https://github.com/jpb06/remix-effect-errors","commit_stats":null,"previous_names":["jpb06/remix-effect-errors"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/jpb06/remix-effect-errors","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jpb06%2Fremix-effect-errors","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jpb06%2Fremix-effect-errors/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jpb06%2Fremix-effect-errors/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jpb06%2Fremix-effect-errors/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jpb06","download_url":"https://codeload.github.com/jpb06/remix-effect-errors/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jpb06%2Fremix-effect-errors/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267278631,"owners_count":24063252,"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","status":"online","status_checked_at":"2025-07-26T02:00:08.937Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["effect-ts","error-reporting","panda-css","react","remix","source-maps"],"created_at":"2024-11-13T03:08:42.662Z","updated_at":"2025-10-09T10:16:50.586Z","avatar_url":"https://github.com/jpb06.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# remix-effect-errors\n\nToying with [remix](https://remix.run/docs/en/main) and [effect](https://effect.website/docs/introduction) to get some fancy errors reporting using [effect-errors](https://github.com/jpb06/effect-errors).\n\n\u003c!-- readme-package-icons start --\u003e\n\n\u003cp align=\"left\"\u003e\u003ca href=\"https://docs.github.com/en/actions\" target=\"_blank\"\u003e\u003cimg height=\"50\" width=\"50\" src=\"https://raw.githubusercontent.com/jpb06/jpb06/master/icons/GithubActions-Dark.svg\" /\u003e\u003c/a\u003e\u0026nbsp;\u003ca href=\"https://www.typescriptlang.org/docs/\" target=\"_blank\"\u003e\u003cimg height=\"50\" width=\"50\" src=\"https://raw.githubusercontent.com/jpb06/jpb06/master/icons/TypeScript.svg\" /\u003e\u003c/a\u003e\u0026nbsp;\u003ca href=\"https://nodejs.org/en/docs/\" target=\"_blank\"\u003e\u003cimg height=\"50\" width=\"50\" src=\"https://raw.githubusercontent.com/jpb06/jpb06/master/icons/NodeJS-Dark.svg\" /\u003e\u003c/a\u003e\u0026nbsp;\u003ca href=\"https://bun.sh/docs\" target=\"_blank\"\u003e\u003cimg height=\"50\" width=\"50\" src=\"https://raw.githubusercontent.com/jpb06/jpb06/master/icons/Bun-Dark.svg\" /\u003e\u003c/a\u003e\u0026nbsp;\u003ca href=\"https://biomejs.dev/guides/getting-started/\" target=\"_blank\"\u003e\u003cimg height=\"50\" width=\"50\" src=\"https://raw.githubusercontent.com/jpb06/jpb06/master/icons/Biome-Dark.svg\" /\u003e\u003c/a\u003e\u0026nbsp;\u003ca href=\"https://panda-css.com/docs/overview/getting-started/\" target=\"_blank\"\u003e\u003cimg height=\"50\" width=\"50\" src=\"https://raw.githubusercontent.com/jpb06/jpb06/master/icons/PandaCss.svg\" /\u003e\u003c/a\u003e\u0026nbsp;\u003ca href=\"https://reactjs.org/docs/getting-started.html\" target=\"_blank\"\u003e\u003cimg height=\"50\" width=\"50\" src=\"https://raw.githubusercontent.com/jpb06/jpb06/master/icons/React-Dark.svg\" /\u003e\u003c/a\u003e\u0026nbsp;\u003ca href=\"https://remix.run/docs/en/v1\" target=\"_blank\"\u003e\u003cimg height=\"50\" width=\"50\" src=\"https://raw.githubusercontent.com/jpb06/jpb06/master/icons/Remix-Dark.svg\" /\u003e\u003c/a\u003e\u0026nbsp;\u003ca href=\"https://vitejs.dev/guide/\" target=\"_blank\"\u003e\u003cimg height=\"50\" width=\"50\" src=\"https://raw.githubusercontent.com/jpb06/jpb06/master/icons/Vite-Dark.svg\" /\u003e\u003c/a\u003e\u0026nbsp;\u003ca href=\"https://www.effect.website/docs/quickstart\" target=\"_blank\"\u003e\u003cimg height=\"50\" width=\"50\" src=\"https://raw.githubusercontent.com/jpb06/jpb06/master/icons/Effect-Dark.svg\" /\u003e\u003c/a\u003e\u0026nbsp;\u003ca href=\"https://www.framer.com/motion/introduction/\" target=\"_blank\"\u003e\u003cimg height=\"50\" width=\"50\" src=\"https://raw.githubusercontent.com/jpb06/jpb06/master/icons/FramerMotion-Dark.svg\" /\u003e\u003c/a\u003e\u003c/p\u003e\n\n\u003c!-- readme-package-icons end --\u003e\n\n## ⚡ So how does that work?\n\nWe basically need two things on remix to achieve our goal:\n\n- A custom remix loader accepting an effect and throwing effect errors details.\n- An Error boundary to display that information if an error occurs.\n\n### 🔶 Creating a custom loader\n\n```typescript\nimport type { LoaderFunctionArgs } from '@remix-run/server-runtime';\nimport { Effect, pipe } from 'effect';\n\nimport { collectErrorDetails } from './logic/collect-error-details';\nimport { remixThrow } from './logic/remix-throw';\n\nexport const effectLoader =\n  \u003cA, E\u003e(effect: (args: LoaderFunctionArgs) =\u003e Effect.Effect\u003cA, E\u003e) =\u003e\n  async (args: LoaderFunctionArgs) =\u003e\n    await Effect.runPromise(\n      pipe(\n        effect(args),\n        Effect.map((data) =\u003e ({ _tag: 'success' as const, data })),\n        Effect.sandbox,\n        Effect.catchAll(collectErrorDetails),\n      ),\n    ).then(remixThrow);\n```\n\nIf the effect fails, we retrieve errors data and related code: \n\n- In dev mode, effect-errors will use sourcemaps to extract code excerpts related to the error.\n- In production however, we must fetch the map file (uploaded in our example on cloudflare R2), and read it to extract sources.\n\n```typescript\nexport const collectErrorDetails = \u003cE\u003e(cause: Cause\u003cE\u003e) =\u003e\n  pipe(\n    Effect.gen(function* () {\n      // Serverside logging\n      const errorsText = prettyPrint(cause, { stripCwd: false });\n      console.error(errorsText);\n\n      const { errors } = yield* captureErrors(cause, {});\n\n      if (errors.every((e) =\u003e e.location !== undefined)) {\n        // Fetch map file and resolve sourcemaps ...\n        const errorsWithSources = yield* getErrorSourcesFromMapFile(errors);\n\n        return yield* Effect.succeed({\n          _tag: 'effect-post-mapped-errors' as const,\n          errors: errorsWithSources,\n        });\n      }\n\n      // in Dev mode, sources are resolved by effect-errors\n      return yield* Effect.succeed({\n        _tag: 'effect-natively-mapped-errors' as const,\n        errors,\n      });\n    }),\n    Effect.scoped,\n    Effect.provide(FetchHttpClient.layer),\n    Effect.withSpan('collect-error-details'),\n  );\n\n```\n\nWe need to pipe on the promise because remix expects us to throw a `json` function result from the loader for errors:\n\n```typescript\nimport { json } from '@remix-run/server-runtime';\n\nimport { Match } from 'effect';\nimport type {\n  EffectLoaderError,\n  EffectLoaderSuccess,\n} from '../types/effect-loader.types';\n\ntype RemixThrowInput\u003cA\u003e = EffectLoaderSuccess\u003cA\u003e | EffectLoaderError;\n\nconst effectHasSucceeded = \u003cA\u003e(\n  p: RemixThrowInput\u003cA\u003e,\n): p is EffectLoaderSuccess\u003cA\u003e =\u003e p._tag === 'success';\n\nexport const remixThrow = \u003cA\u003e(input: RemixThrowInput\u003cA\u003e) =\u003e\n  Match.value(input).pipe(\n    Match.when(effectHasSucceeded, ({ data }) =\u003e data),\n    Match.orElse((data) =\u003e {\n      throw json(data, { status: 500 });\n    }),\n  );\n```\n\n### 🔶 Creating an error boundary to display effect errors details\n\nFirst, let's create a hook to get errors data:\n\n```typescript\nimport {\n  isRouteErrorResponse,\n  useLocation,\n  useRouteError,\n} from '@remix-run/react';\n\nimport type {\n  EffectNativelyMappedErrors,\n  EffectPostMappedErrors,\n} from '@server/loader/types/effect-loader.types';\n\nimport { isUnknownAnEffectError } from './logic/is-uknown-an-effect-error.logic';\nimport { mapEffectErrorTypes } from './logic/map-effect-error-types';\n\nexport type EffectPostMappedErrorsWithPath = EffectPostMappedErrors \u0026 {\n  path: string;\n};\nexport type EffectNativelyMappedErrorsWithPath = EffectNativelyMappedErrors \u0026 {\n  path: string;\n};\n\nexport type ErrorsDetails =\n  | {\n      _tag: 'route' | 'error' | 'unknown';\n      path: string;\n      errors: {\n        message: string;\n      }[];\n    }\n  | EffectPostMappedErrorsWithPath\n  | EffectNativelyMappedErrorsWithPath;\n\nexport const useErrorDetails = (): ErrorsDetails =\u003e {\n  const { pathname } = useLocation();\n  const error = useRouteError();\n\n  if (isUnknownAnEffectError(error)) {\n    return mapEffectErrorTypes(error, pathname);\n  }\n\n  const isRoute = isRouteErrorResponse(error);\n  if (isRoute) {\n    return {\n      _tag: 'route' as const,\n      path: pathname,\n      errors: [\n        {\n          message: `${error.statusText}`,\n        },\n      ],\n    };\n  }\n\n  if (error instanceof Error) {\n    return {\n      _tag: 'error' as const,\n      path: pathname,\n      errors: [error],\n    };\n  }\n\n  return {\n    _tag: 'unknown' as const,\n    path: pathname,\n    errors: [{ message: 'Unknown Error' }],\n  };\n};\n```\n\nWe can then focus on displaying our errors data...\n\n```typescript\nimport { AppErrors } from './children/app-errors';\nimport { Summary } from './children/summary';\nimport { errorBoundaryStyles } from './error-boundary.styles';\nimport { useErrorDetails } from './hooks/use-error-details';\n\nexport const ErrorBoundary = () =\u003e {\n  const css = errorBoundaryStyles();\n\n  const data = useErrorDetails();\n\n  return (\n    \u003cdiv className={css.root}\u003e\n      \u003cSummary {...data} /\u003e\n      \u003cAppErrors {...data} /\u003e\n    \u003c/div\u003e\n  );\n};\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjpb06%2Fremix-effect-errors","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjpb06%2Fremix-effect-errors","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjpb06%2Fremix-effect-errors/lists"}