Some sort of POC to improve the way Effect reports errors in a dev env 🤔
Had to re-export runSync and runPromise to apply prettyPrint function on the cause returned by a catchAll.
So using it would look like this :
import { runPromise } from 'effect-errors'; await runPromise( Effect.gen(function* () { // ... }), );You can also directly import the prettyPrint function to do whatever you want with it 🤷
import { prettyPrint } from 'effect-errors'; await Effect.runPromise( pipe( Effect.gen(function* () { // ... }), Effect.sandbox, Effect.catchAll((e) => { console.error(prettyPrint(e)); return Effect.fail('❌ runPromise failure'); }), ), );Signature is the following:
const prettyPrint: <E>(cause: Cause<E>, options?: PrettyPrintOptions) => stringPrettyPrintOptions allows you to tweak the following:
default:
true
default:
false(absolute paths)
default:
true
You can also use the function prettyPrintFromCapturedErrors to display errors from captured errors:
import { NodeFileSystem } from '@effect/platform-node'; import { Effect, pipe } from 'effect'; import { TaggedError } from 'effect/Data'; import { captureErrors, prettyPrintFromCapturedErrors } from 'effect-errors'; export class MyCustomError extends TaggedError('MyCustomError')<{ cause?: unknown; message?: string; }> {} const task = pipe( Effect.fail( new MyCustomError({ cause: 'Well this sucks', }), ), Effect.withSpan('task', { attributes: { isCool: true }, }), ); const program = pipe( task, Effect.sandbox, Effect.catchAll((e) => Effect.gen(function* () { const errors = yield* captureErrors(e); const message = prettyPrintFromCapturedErrors(errors, { stripCwd: true, hideStackTrace: true, }); console.error(message); // Do something with the captured errors ... }), ), Effect.provide(NodeFileSystem.layer), Effect.withSpan('program', { attributes: { name: 'cool' } }), ); Effect.runPromise(program);The result would look like so:
The best way is to use either SchemaError or TaggedError.
Declaring the error could look like this:
import { Schema } from 'effect'; export class FileNotFoundError extends Schema.TaggedError<SchemaError>()( 'FileNotFound', { cause: Schema.Defect, }, ) {}You would then raise a FileNotFoundError to the error channel like this:
Effect.tryPromise({ try: () => ..., catch: (e) => new FileNotFoundError({ cause: e }), }); // or raising directly Effect.fail(new FileNotFoundError({ cause: "Oh no!" }));export class UserNotFoundError extends TaggedError('UserNotFound')<{ cause?: unknown; }> {}You would then raise a UserNotFoundError to the error channel like this:
Effect.tryPromise({ try: () => ..., catch: (e) => new UserNotFoundError({ cause: e }), }); // or raising directly Effect.fail(new UserNotFoundError({ cause: "User does not exist" }));Alternatively, you can use a plain object with a _tag and message attribute, but you won't get any stacktrace if you use this method:
Effect.fail({ _tag: 'SucksToBeMe', message: 'Yeah...' });You might want to apply your own logic to reported errors data; for example if you want to display errors in html. You can do so using captureErrors. The function has the following signature:
export interface ErrorSpan { name: string; attributes: Record<string, unknown>; durationInMilliseconds: number | undefined; startTime: bigint; endTime: bigint | undefined; } export interface ErrorData { errorType: unknown; message: unknown; stack: string[] | undefined; sources: Omit<ErrorRelatedSources, '_tag'>[] | undefined; location: Omit<ErrorLocation, '_tag'>[] | undefined; spans: ErrorSpan[] | undefined; isPlainString: boolean; } export interface CapturedErrors { interrupted: boolean; errors: ErrorData[]; } export interface CaptureErrorsOptions { stripCwd?: boolean; } const captureErrors: <E>( cause: Cause<E>, options?: CaptureErrorsOptions ) => Effect.Effect<CapturedErrors, FsError, FileSystem>You can use captureErrors like so:
import { captureErrors } from 'effect-errors'; import { NodeFileSystem } from '@effect/platform-node'; await Effect.runPromise( pipe( effect, Effect.sandbox, Effect.catchAll((e) => Effect.gen(function* () { const errors = yield* captureErrors(e); // ... }), ), Effect.provide(NodeFileSystem.layer), ), );Capturing errors from the from-promise bundle would return something like this, for example:
{ "interrupted": false, "errors": [ { "errorType": "FetchError", "message": { "code": "ConnectionRefused", "path": "https://yolo-bro-oh-no.org/users/123", "errno": 0 }, "stack": [ "at new e (:1:28)", "at new <anonymous> (./src/tests/bundle/from-promise.js:31:85172)", "at new t (:1:28)", "at new Ga (:1:28)", "at catch (./src/tests/bundle/from-promise.js:37:352)", "at Sync (./src/tests/bundle/from-promise.js:31:39923)", "at runLoop (./src/tests/bundle/from-promise.js:31:42686)", "at evaluateEffect (./src/tests/bundle/from-promise.js:31:38196)", "at evaluateMessageWhileSuspended (./src/tests/bundle/from-promise.js:31:37872)", "at drainQueueOnCurrentThread (./src/tests/bundle/from-promise.js:31:35561)", "at run (./src/tests/bundle/from-promise.js:31:43020)", "at starveInternal (./src/tests/bundle/from-promise.js:31:6243)", "at processTicksAndRejections (:12:39)" ], "sources": [ { "name": "FetchError", "runPath": "/Users/jpb06/repos/perso/effect-errors/src/tests/bundle/from-promise.js:37:352", "sourcesPath": "/Users/jpb06/repos/perso/effect-errors/src/examples/from-promise.ts:30:13", "source": [ { "line": 27, "code": " try: async () =>" }, { "line": 28, "code": " await fetch(`https://yolo-bro-oh-no.org/users/${userId}`)," }, { "line": 29, "code": " catch: (e) =>" }, { "line": 30, "code": " new FetchError({", "column": 13 }, { "line": 31, "code": " cause: e," }, { "line": 32, "code": " })," }, { "line": 33, "code": " })," } ] }, { "name": "fetchTask", "runPath": "/Users/jpb06/repos/perso/effect-errors/src/tests/bundle/from-promise.js:37:213", "sourcesPath": "/Users/jpb06/repos/perso/effect-errors/src/examples/from-promise.ts:25:10", "source": [ { "line": 22, "code": ");" }, { "line": 23, "code": "" }, { "line": 24, "code": "const fetchTask = (userId: string) =>" }, { "line": 25, "code": " Effect.withSpan('fetch-user', { attributes: { userId } })(", "column": 10 }, { "line": 26, "code": " Effect.tryPromise({" }, { "line": 27, "code": " try: async () =>" }, { "line": 28, "code": " await fetch(`https://yolo-bro-oh-no.org/users/${userId}`)," } ] }, { "name": "fromPromiseTask", "runPath": "/Users/jpb06/repos/perso/effect-errors/src/tests/bundle/from-promise.js:37:490", "sourcesPath": "/Users/jpb06/repos/perso/effect-errors/src/examples/from-promise.ts:44:39", "source": [ { "line": 41, "code": " })," }, { "line": 42, "code": " );" }, { "line": 43, "code": "" }, { "line": 44, "code": "export const fromPromiseTask = Effect.withSpan('from-promise-task')(", "column": 39 }, { "line": 45, "code": " Effect.gen(function* () {" }, { "line": 46, "code": " yield* filename(fileName);" }, { "line": 47, "code": "" } ] } ], "spans": [ { "name": "fromPromiseTask", "attributes": {}, "durationInMilliseconds": 246 }, { "name": "fetchUser", "attributes": { "userId": "123" }, "durationInMilliseconds": 239 } ], "isPlainString": false } ] }If no map file is found, a location array will be returned instead of sources:
{ "interrupted": false, "errors": [ { "errorType": "FetchError", "message": "request to https://yolo-bro-oh-no.org/users/1 failed, reason: getaddrinfo ENOTFOUND yolo-bro-oh-no.org", "stack": [ "at catcher (file:///Users/jpb06/repos/remix-effect-errors/build/server/nodejs-eyJydW50aW1lIjoibm9kZWpzIn0/index.js?t=1729013117205.3699:2:14550)", "at EffectPrimitive.effect_instruction_i0 (file:///Users/jpb06/repos/remix-effect-errors/node_modules/effect/src/internal/core-effect.ts:1694:56)", "at body (file:///Users/jpb06/repos/remix-effect-errors/node_modules/effect/src/internal/fiberRuntime.ts:1113:41)", "at Object.effect_internal_function (file:///Users/jpb06/repos/remix-effect-errors/node_modules/effect/src/Utils.ts:780:14)", "at internalCall (file:///Users/jpb06/repos/remix-effect-errors/node_modules/effect/src/Utils.ts:784:22)", "at FiberRuntime.Sync (file:///Users/jpb06/repos/remix-effect-errors/node_modules/effect/src/internal/fiberRuntime.ts:1113:19)", "at f (file:///Users/jpb06/repos/remix-effect-errors/node_modules/effect/src/internal/fiberRuntime.ts:1347:53)", "at Object.context (file:///Users/jpb06/repos/remix-effect-errors/node_modules/effect/src/internal/tracer.ts:93:19)", "at FiberRuntime.runLoop (file:///Users/jpb06/repos/remix-effect-errors/node_modules/effect/src/internal/fiberRuntime.ts:1337:34)", "at FiberRuntime.evaluateEffect (file:///Users/jpb06/repos/remix-effect-errors/node_modules/effect/src/internal/fiberRuntime.ts:900:27)" ], "location": [ { "filePath": "/build/server/nodejs-eyJydW50aW1lIjoibm9kZWpzIn0/index.js", "line": 2, "column": 14414 }, { "filePath": "/build/server/nodejs-eyJydW50aW1lIjoibm9kZWpzIn0/index.js", "line": 2, "column": 14703 }, { "filePath": "/build/server/nodejs-eyJydW50aW1lIjoibm9kZWpzIn0/index.js", "line": 2, "column": 17636 } ], "spans": [ { "name": "fetch-user", "attributes": { "userId": 1 }, "durationInMilliseconds": 52 }, { "name": "from-promise-task", "attributes": {}, "durationInMilliseconds": 54 }, { "name": "promise-example-loader", "attributes": { "url": "http://localhost:3000/promise", "method": "GET", "body": null }, "durationInMilliseconds": 55 } ], "isPlainString": false } ] }I wrote some examples for fun and giggles. You can run them using:
pnpm run-examplesYou can check this example using remix error boundaries.

