Security News
RubyGems.org Adds New Maintainer Role
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
effect-errors
Advanced tools
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) => string
PrettyPrintOptions
allows you to tweak the following:
enabled
- Whether pretty printing is enabled or notdefault:
true
stripCwd
- Whether spans and stacktrace should contain absolute or relative pathsdefault:
false
(absolute paths)
reverseSpans
- Whether spans order should reversed (entry point first instead of inner callee first)default:
true
(entry point first)
The best way is to use either SchemaError
or TaggedError
.
SchemaError
Declaring the error could look like this:
import { Schema } from '@effect/schema';
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" }));
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 {
reverseSpans?: boolean;
stripCwd?: boolean;
}
const captureErrors: <E>(
cause: Cause<E>,
options?: CaptureErrorsOptions
) => Effect.Effect<CapturedErrors, FsError>
You can use captureErrors
like so:
import { captureErrors } from 'effect-errors';
await Effect.runPromise(
pipe(
effect,
Effect.catchAll((e) =>
Effect.gen(function* () {
const errors = yield* captureErrors(e);
// ...
}),
),
),
);
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": [
{
"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": " }),"
}
]
},
{
"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('fetchUser', { 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}`),"
}
]
},
{
"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('fromPromiseTask')(",
"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
}
]
}
runPromise
/ runSync
I wrote some examples for fun and giggles. You can run them using:
bun run-examples
captureErrors
You can check this example using remix error boundaries.
FAQs
A POC for errors reporting in Effect
We found that effect-errors demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
Security News
Node.js will be enforcing stricter semver-major PR policies a month before major releases to enhance stability and ensure reliable release candidates.
Security News
Research
Socket's threat research team has detected five malicious npm packages targeting Roblox developers, deploying malware to steal credentials and personal data.