GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GitHub - jpb06/effect-errors: Fancy errors reporting for effect

BranchesTags
Open more actions menu

Latest commit

 

History

780 Commits
780 Commits

Folders and files

NameName
Last commit message
Last commit date
.github/workflows
.github/workflows
 
 
.vscode
.vscode
 
 
docs
docs
 
 
release-please
release-please
 
 
src
src
 
 
.gitignore
.gitignore
 
 
.node-version
.node-version
 
 
CHANGELOG.md
CHANGELOG.md
 
 
LICENSE
LICENSE
 
 
README.md
README.md
 
 
biome.json
biome.json
 
 
package.json
package.json
 
 
pnpm-lock.yaml
pnpm-lock.yaml
 
 
renovate.json
renovate.json
 
 
sonar-project.properties
sonar-project.properties
 
 
tsconfig.cjs.json
tsconfig.cjs.json
 
 
tsconfig.esm.json
tsconfig.esm.json
 
 
tsconfig.json
tsconfig.json
 
 
vite.config.mts
vite.config.mts
 
 

Repository files navigation

effect-errors

Open in Visual Studio Code Last commit npm downloads npm bundle size Github workflow Quality Gate Status Maintainability Rating Security Rating Reliability Rating Coverage Lines of Code Technical Debt Code Smells Bugs Vulnerabilities Duplicated Lines (%)

Some sort of POC to improve the way Effect reports errors in a dev env 🤔

       

example

⚡ So how does it work?

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) => string

PrettyPrintOptions allows you to tweak the following:

enabled - Whether pretty printing is enabled or not

default: true

stripCwd - Whether spans and stacktrace should contain absolute or relative paths

default: false (absolute paths)

hideStackTrace - Whether node stacktrace should be displayed

default: true

⚡ Pretty printing from captured errors

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:

example

⚡ How should I raise errors?

The best way is to use either SchemaError or TaggedError.

🔶 SchemaError

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!" }));

🔶 TaggedError

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" }));

🔶 Plain object

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...' });

⚡ Capturing errors data

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 catch (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
    }
  ]
}

⚡ examples

🔶 error logging - runPromise / runSync

I wrote some examples for fun and giggles. You can run them using:

pnpm run-examples

🔶 Custom display for errors - captureErrors

You can check this example using remix error boundaries.

About

Fancy errors reporting for effect

remix-effect-errors.vercel.app/promise

Topics

effect-tserror-reportingpocsource-mapsspans

Resources

Readme
MIT license
Activity

Stars

19 stars

Watchers

1 watching

Forks

0 forks
Report repository

Releases

Packages

Used by

Contributors

Languages

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.