1/**
2 * Reports Effect failures to external code.
3 *
4 * An `ErrorReporter` receives `Cause` values from `Effect.withErrorReporting`,
5 * manual `report` calls, or built-in reporting boundaries. It forwards each
6 * non-interruption error to a callback, so applications can send failures to
7 * logging, monitoring, or error-tracking systems. This module also includes
8 * layers for installing reporters and symbols for marking errors as ignored or
9 * attaching severity and attributes.
10 *
11 * @since 4.0.0
12 */
13import type * as Cause from "./Cause.ts"
14import type * as Context from "./Context.ts"
15import * as Effect from "./Effect.ts"
16import type * as Fiber from "./Fiber.ts"
17import * as effect from "./internal/effect.ts"
18import * as references from "./internal/references.ts"
19import * as Layer from "./Layer.ts"
20import * as LogLevel from "./LogLevel.ts"
21import type { Severity } from "./LogLevel.ts"
22import type { ReadonlyRecord } from "./Record.ts"
23import type * as Scope from "./Scope.ts"
24
25/**
26 * String literal type used as the runtime type identifier for
27 * `ErrorReporter` values.
28 *
29 * **When to use**
30 *
31 * Use to refer to the runtime type identifier type in low-level integrations.
32 *
33 * @category type IDs
34 * @since 4.0.0
35 */
36export type TypeId = "~effect/ErrorReporter"
37
38/**
39 * Runtime type identifier attached to `ErrorReporter` values.
40 *
41 * **Details**
42 *
43 * This marker is part of the runtime representation of `ErrorReporter`
44 * implementations. Most code should create reporters with `make` and register
45 * them with `layer`.
46 *
47 * @category type IDs
48 * @since 4.0.0
49 */
50export const TypeId: TypeId = "~effect/ErrorReporter"
51
52/**
53 * An `ErrorReporter` receives reported failures and forwards them to an
54 * external system such as a logging service or error tracker.
55 *
56 * **When to use**
57 *
58 * Use as the interface for custom reporters that forward reported Effect
59 * failures to logging, monitoring, or error-tracking systems.
60 *
61 * **Details**
62 *
63 * Reporting is triggered by `Effect.withErrorReporting`,
64 * `ErrorReporter.report`, or built-in boundaries in the HTTP and RPC server
65 * modules. Use {@link make} to create a reporter; it handles deduplication
66 * and per-error annotation extraction automatically.
67 *
68 * @see {@link make} for creating an `ErrorReporter` from a callback
69 * @see {@link layer} for registering reporters in the environment
70 * @see {@link report} for manually reporting a `Cause`
71 * @see {@link Effect.withErrorReporting} for reporting failures from an effect
72 *
73 * @category services
74 * @since 4.0.0
75 */
76export interface ErrorReporter {
77 readonly [TypeId]: TypeId
78 report(options: {
79 readonly cause: Cause.Cause<unknown>
80 readonly fiber: Fiber.Fiber<unknown, unknown>
81 readonly timestamp: bigint
82 }): void
83}
84
85/**
86 * Creates an `ErrorReporter` from a callback.
87 *
88 * **When to use**
89 *
90 * Use to define how reported failures are forwarded to a logging, monitoring,
91 * or error-tracking backend.
92 *
93 * **Details**
94 *
95 * The returned reporter automatically deduplicates causes and individual
96 * errors (the same object is never reported twice), skips interruptions,
97 * and resolves the `ignore`, `severity`, and `attributes` annotations on
98 * each error before invoking your callback.
99 *
100 * **Example** (Forwarding errors to a callback)
101 *
102 * ```ts import.meta.vitest
103 * import { Effect, ErrorReporter } from "effect"
104 *
105 * const reports: Array<{ message: string; severity: string; attributes: object }> = []
106 * const reporter = ErrorReporter.make(({ error, severity, attributes }) => {
107 * reports.push({ message: error.message, severity, attributes })
108 * })
109 *
110 * const program = Effect.fail(new Error("boom")).pipe(
111 * Effect.withErrorReporting,
112 * Effect.provide(ErrorReporter.layer([reporter])),
113 * Effect.exit
114 * )
115 *
116 * await Effect.runPromise(program)
117 * reports // => [{ message: "boom", severity: "Info", attributes: {} }]
118 * ```
119 *
120 * @see {@link layer} for registering reporters in the environment
121 * @see {@link report} for manually reporting a `Cause`
122 *
123 * @category constructors
124 * @since 4.0.0
125 */
126export const make = (
127 report: (options: {
128 readonly cause: Cause.Cause<unknown>
129 readonly error: Error
130 readonly attributes: ReadonlyRecord<string, unknown>
131 readonly severity: Severity
132 readonly fiber: Fiber.Fiber<unknown, unknown>
133 readonly timestamp: bigint
134 }) => void
135): ErrorReporter => {
136 const reported = new WeakSet<Cause.Cause<unknown> | object>()
137 return {
138 [TypeId]: TypeId,
139 report(options) {
140 if (reported.has(options.cause)) return
141 reported.add(options.cause)
142 for (let i = 0; i < options.cause.reasons.length; i++) {
143 const reason = options.cause.reasons[i]
144 if (reason._tag === "Interrupt") continue
145 const original = reason._tag === "Fail" ? reason.error : reason.defect
146 const isObject = typeof original === "object" && original !== null
147 if (isObject) {
148 if (reported.has(original)) continue
149 reported.add(original)
150 }
151 if (isIgnored(original)) continue
152 const pretty = effect.causePrettyError(original as any, reason.annotations)
153 report({
154 ...options,
155 error: pretty,
156 severity: isObject ? getSeverity(original) : "Info",
157 attributes: isObject ? getAttributes(original) : emptyAttributes
158 })
159 }
160 }
161 }
162}
163
164/**
165 * Context reference that holds the set of active error reporters for the
166 * current fiber. Defaults to an empty set (no reporting).
167 *
168 * **When to use**
169 *
170 * Use when you need to read or replace the current set of error reporters
171 * directly.
172 *
173 * @category services
174 * @since 4.0.0
175 */
176export const CurrentErrorReporters: Context.Reference<ReadonlySet<ErrorReporter>> = references.CurrentErrorReporters
177
178/**
179 * Creates a `Layer` that registers one or more `ErrorReporter`s.
180 *
181 * **When to use**
182 *
183 * Use to provide one or more error reporters to effects that perform error
184 * reporting.
185 *
186 * **Details**
187 *
188 * Reporters can be plain `ErrorReporter` values or effectful
189 * `Effect<ErrorReporter>` values that are resolved when the layer is built. By
190 * default the provided reporters **replace** any previously registered
191 * reporters. Set `mergeWithExisting: true` to add them alongside existing ones.
192 *
193 * **Example** (Providing error reporters)
194 *
195 * ```ts import.meta.vitest
196 * import { Effect, ErrorReporter } from "effect"
197 *
198 * const reports: Array<string> = []
199 * const firstReporter = ErrorReporter.make(({ error, severity }) => {
200 * reports.push(`[${severity}] ${error.message}`)
201 * })
202 * const secondReporter = ErrorReporter.make(({ error, severity }) => {
203 * reports.push(`${severity}: ${error.message}`)
204 * })
205 *
206 * // Replace all existing reporters
207 * const ReporterLayer = ErrorReporter.layer([
208 * firstReporter,
209 * secondReporter
210 * ])
211 *
212 * // Add to existing reporters instead of replacing
213 * const ReporterMerged = ErrorReporter.layer(
214 * [secondReporter],
215 * { mergeWithExisting: true }
216 * )
217 *
218 * const program = Effect.fail("boom").pipe(
219 * Effect.withErrorReporting,
220 * Effect.provide(ReporterLayer),
221 * Effect.exit
222 * )
223 *
224 * await Effect.runPromise(program)
225 * reports // => ["[Info] boom", "Info: boom"]
226 * ```
227 *
228 * @see {@link make} for creating an `ErrorReporter` from a callback
229 * @see {@link CurrentErrorReporters} for low-level access to the current reporters
230 *
231 * @category layers
232 * @since 4.0.0
233 */
234export const layer = <
235 const Reporters extends ReadonlyArray<ErrorReporter | Effect.Effect<ErrorReporter, any, any>>
236>(
237 reporters: Reporters,
238 options?: { readonly mergeWithExisting?: boolean | undefined } | undefined
239): Layer.Layer<
240 never,
241 Reporters extends readonly [] ? never : Effect.Error<Reporters[number]>,
242 Exclude<
243 Reporters extends readonly [] ? never : Effect.Services<Reporters[number]>,
244 Scope.Scope
245 >
246> =>
247 Layer.effect(
248 CurrentErrorReporters,
249 Effect.withFiber(Effect.fnUntraced(function*(fiber) {
250 const currentReporters = new Set(
251 options?.mergeWithExisting === true ? fiber.getRef(references.CurrentErrorReporters) : []
252 )
253 for (const reporter of reporters) {
254 currentReporters.add(Effect.isEffect(reporter) ? yield* reporter : reporter)
255 }
256 return currentReporters
257 }))
258 )
259
260/**
261 * Runs all registered error reporters on the current fiber for a `Cause`.
262 *
263 * **When to use**
264 *
265 * Use to report a failure for observability without failing the current fiber.
266 *
267 * **Example** (Reporting a cause manually)
268 *
269 * ```ts import.meta.vitest
270 * import { Cause, Effect, ErrorReporter } from "effect"
271 *
272 * const messages: Array<string> = []
273 * const program = Effect.gen(function*() {
274 * const cause = Cause.fail("something went wrong")
275 * yield* ErrorReporter.report(cause)
276 * return "fallback value"
277 * })
278 *
279 * const reporter = ErrorReporter.make(({ error }) => messages.push(error.message))
280 * const output = await Effect.runPromise(
281 * Effect.provide(program, ErrorReporter.layer([reporter]))
282 * )
283 * messages // => ["something went wrong"]
284 * output // => "fallback value"
285 * ```
286 *
287 * @category logging
288 * @since 4.0.0
289 */
290export const report = <E>(cause: Cause.Cause<E>): Effect.Effect<void> =>
291 Effect.withFiber((fiber) => {
292 effect.reportCauseUnsafe(fiber, cause)
293 return Effect.void
294 })
295
296/**
297 * Interface that object errors can implement to control reporting behavior.
298 *
299 * **When to use**
300 *
301 * Use as the annotation contract for object errors that customize how error
302 * reporting handles them.
303 *
304 * **Details**
305 *
306 * All three annotation properties are optional: `[ErrorReporter.ignore]`
307 * prevents reporting when set to `true`, `[ErrorReporter.severity]` overrides
308 * the default `"Info"` severity, and `[ErrorReporter.attributes]` adds extra
309 * key/value pairs forwarded to reporters. The global `Error` interface is
310 * augmented with `Reportable`, so these properties are available on `Error`
311 * instances at the type level.
312 *
313 * @see {@link ignore} for the runtime annotation key that suppresses reports
314 * @see {@link severity} for the runtime annotation key that overrides severity
315 * @see {@link attributes} for the runtime annotation key that attaches reporter
316 * metadata
317 *
318 * @category annotations
319 * @since 4.0.0
320 */
321export interface Reportable {
322 readonly [ignore]?: boolean
323 readonly [severity]?: Severity
324 readonly [attributes]?: ReadonlyRecord<string, unknown>
325}
326
327declare global {
328 interface Error extends Reportable {}
329}
330
331/**
332 * Defines the string property key used to mark an object error as ignored by error
333 * reporting.
334 *
335 * **When to use**
336 *
337 * Use to type the property key that suppresses reporting for expected object
338 * errors.
339 *
340 * **Details**
341 *
342 * Set this property to `true` on an error class or object error to prevent it
343 * from being forwarded to reporters. This is useful for expected failures such
344 * as HTTP 404 responses.
345 *
346 * @category annotations
347 * @since 4.0.0
348 */
349export type ignore = "~effect/ErrorReporter/ignore"
350
351/**
352 * Defines the runtime property key used to mark an object error as ignored by error
353 * reporting.
354 *
355 * **When to use**
356 *
357 * Use to suppress reporting for expected object errors, such as HTTP 404
358 * responses.
359 *
360 * **Details**
361 *
362 * Set `error[ErrorReporter.ignore]` to `true` to prevent the error from being
363 * forwarded to reporters. This is useful for expected failures such as HTTP 404
364 * responses.
365 *
366 * **Example** (Marking errors as ignored)
367 *
368 * ```ts import.meta.vitest
369 * import { Data, ErrorReporter } from "effect"
370 *
371 * class NotFoundError extends Data.TaggedError("NotFoundError")<{}> {
372 * readonly [ErrorReporter.ignore] = true
373 * }
374 *
375 * ErrorReporter.isIgnored(new NotFoundError()) // => true
376 * ```
377 *
378 * @see {@link isIgnored} for checking whether a value carries this annotation
379 * @see {@link Reportable} for the annotation contract recognized on object
380 * errors
381 *
382 * @category annotations
383 * @since 4.0.0
384 */
385export const ignore: ignore = "~effect/ErrorReporter/ignore"
386
387/**
388 * Returns `true` if the given value has the `ErrorReporter.ignore` annotation
389 * set to `true`.
390 *
391 * **When to use**
392 *
393 * Use to check whether an error value is annotated to be skipped before
394 * forwarding it to error reporting code.
395 *
396 * @see {@link ignore} for the annotation key this predicate reads
397 *
398 * @category predicates
399 * @since 4.0.0
400 */
401export const isIgnored = (u: unknown): boolean =>
402 typeof u === "object" && u !== null && ignore in u && u[ignore] === true
403
404/**
405 * Defines the string property key used to override the severity level of an object error.
406 *
407 * **When to use**
408 *
409 * Use to type the property key that overrides the reporting severity for object
410 * errors.
411 *
412 * **Details**
413 *
414 * When set to a valid `LogLevel.Severity`, the reporter callback receives this
415 * value as `severity`. Missing or invalid values fall back to `"Info"`.
416 *
417 * @category annotations
418 * @since 4.0.0
419 */
420export type severity = "~effect/ErrorReporter/severity"
421
422/**
423 * Defines the runtime property key used to override the severity level of an object error.
424 *
425 * **When to use**
426 *
427 * Use to annotate object errors with the severity reporter callbacks should
428 * receive.
429 *
430 * **Details**
431 *
432 * Set `error[ErrorReporter.severity]` to a valid `LogLevel.Severity` value.
433 * Missing or invalid values fall back to `"Info"`.
434 *
435 * **Example** (Setting error severity annotations)
436 *
437 * ```ts import.meta.vitest
438 * import { Data, ErrorReporter } from "effect"
439 *
440 * class DeprecationWarning extends Data.TaggedError("DeprecationWarning")<{}> {
441 * readonly [ErrorReporter.severity] = "Warn" as const
442 * }
443 *
444 * ErrorReporter.getSeverity(new DeprecationWarning()) // => "Warn"
445 * ```
446 *
447 * @see {@link getSeverity} for reading the severity stored under this key
448 * @see {@link Reportable} for the annotation contract recognized on object
449 * errors
450 *
451 * @category annotations
452 * @since 4.0.0
453 */
454export const severity: severity = "~effect/ErrorReporter/severity"
455
456/**
457 * Reads the `ErrorReporter.severity` annotation from an error object,
458 * falling back to `"Info"` when the annotation is unset or invalid.
459 *
460 * **When to use**
461 *
462 * Use to inspect the severity that reporter callbacks will receive for an
463 * object error.
464 *
465 * @see {@link severity} for the annotation key used to override severity
466 * @see {@link Reportable} for the annotation properties recognized on object errors
467 *
468 * @category annotations
469 * @since 4.0.0
470 */
471export const getSeverity = (error: object): Severity => {
472 if (severity in error && LogLevel.values.includes(error[severity] as Severity)) {
473 return error[severity] as Severity
474 }
475 return "Info"
476}
477
478/**
479 * Defines the string property key used to attach extra key/value metadata to an object
480 * error report.
481 *
482 * **When to use**
483 *
484 * Use to type the property key that attaches metadata to object error reports.
485 *
486 * **Details**
487 *
488 * Reporters receive these attributes alongside the error, making it easy to
489 * include contextual information such as user IDs, request IDs, or other
490 * domain-specific debugging data.
491 *
492 * @category annotations
493 * @since 4.0.0
494 */
495export type attributes = "~effect/ErrorReporter/attributes"
496
497/**
498 * Defines the runtime property key used to attach extra key/value metadata to an object
499 * error report.
500 *
501 * **When to use**
502 *
503 * Use to attach domain metadata to object errors so reporter callbacks receive
504 * it with the reported failure.
505 *
506 * **Details**
507 *
508 * Set `error[ErrorReporter.attributes]` to a record of metadata that should be
509 * forwarded to reporters alongside the error.
510 *
511 * **Example** (Setting error attributes)
512 *
513 * ```ts import.meta.vitest
514 * import { Data, ErrorReporter } from "effect"
515 *
516 * class PaymentError extends Data.TaggedError("PaymentError")<{
517 * readonly orderId: string
518 * }> {
519 * readonly [ErrorReporter.attributes] = {
520 * orderId: this.orderId
521 * }
522 * }
523 *
524 * ErrorReporter.getAttributes(new PaymentError({ orderId: "order-123" })) // => { orderId: "order-123" }
525 * ```
526 *
527 * @see {@link ignore} for suppressing reports for expected object errors
528 * @see {@link severity} for overriding reporter severity
529 * @see {@link getAttributes} for reading the metadata stored under this key
530 * @see {@link Reportable} for the annotation contract recognized on object
531 * errors
532 *
533 * @category annotations
534 * @since 4.0.0
535 */
536export const attributes: attributes = "~effect/ErrorReporter/attributes"
537
538/**
539 * Reads the `ErrorReporter.attributes` annotation from an error object,
540 * returning an empty record when unset.
541 *
542 * **When to use**
543 *
544 * Use to inspect the attributes that reporter callbacks will receive for an
545 * object error.
546 *
547 * **Details**
548 *
549 * Returns the value stored under `ErrorReporter.attributes`, or the module's
550 * shared empty record when the annotation is absent.
551 *
552 * **Gotchas**
553 *
554 * The annotation value is returned as-is; this helper does not validate or
555 * clone it.
556 *
557 * @see {@link attributes} for the annotation key used to attach metadata
558 * @see {@link Reportable} for the annotation properties recognized on object errors
559 *
560 * @category annotations
561 * @since 4.0.0
562 */
563export const getAttributes = (error: object): ReadonlyRecord<string, unknown> => {
564 return attributes in error ? error[attributes] as any : emptyAttributes
565}
566
567const emptyAttributes: ReadonlyRecord<string, unknown> = {}