From 502223adf1a36153651e16533d50817d24450454 Mon Sep 17 00:00:00 2001 From: Giulio Canti Date: Sat, 25 Jan 2025 11:21:29 +0100 Subject: [PATCH] Cause: add jsdocs and tests (#4340) --- packages/effect/src/Cause.ts | 1011 +++++++++++++++----- packages/effect/src/internal/cause.ts | 3 +- packages/effect/test/Cause.test.ts | 1223 +++++++++++++++++++------ packages/effect/vitest.config.ts | 2 +- 4 files changed, 1717 insertions(+), 522 deletions(-) diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index c676ed3e744..7587e18d2ba 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -40,124 +40,215 @@ import type { Span } from "./Tracer.js" import type { Covariant, NoInfer } from "./Types.js" /** + * A unique symbol identifying the `Cause` type. + * + * **Details** + * + * This provides a symbol that helps identify instances of the `Cause` data + * type. This can be used for advanced operations such as refining types or + * building internal utilities that check whether an unknown value is a `Cause`. + * + * @see {@link isCause} Check if a value is a `Cause` * @since 2.0.0 - * @category symbols + * @category Symbols */ export const CauseTypeId: unique symbol = internal.CauseTypeId /** * @since 2.0.0 - * @category symbols + * @category Symbols */ export type CauseTypeId = typeof CauseTypeId /** + * A unique symbol identifying the `RuntimeException` type. + * + * **Details** + * + * This provides a symbol that identifies a `RuntimeException`. This is + * typically used internally by the library to recognize checked exceptions that + * occur during runtime. + * + * @see {@link RuntimeException} Create or work with a `RuntimeException` + * * @since 2.0.0 - * @category symbols + * @category Symbols */ export const RuntimeExceptionTypeId: unique symbol = core.RuntimeExceptionTypeId /** * @since 2.0.0 - * @category symbols + * @category Symbols */ export type RuntimeExceptionTypeId = typeof RuntimeExceptionTypeId /** + * A unique symbol identifying the `InterruptedException` type. + * + * **Details** + * + * This provides a symbol that identifies an `InterruptedException`. This is + * typically used internally to recognize when a fiber has been interrupted, + * helping the framework handle interruption logic correctly. + * + * @see {@link InterruptedException} Create or work with an `InterruptedException` + * * @since 2.0.0 - * @category symbols + * @category Symbols */ export const InterruptedExceptionTypeId: unique symbol = core.InterruptedExceptionTypeId /** * @since 2.0.0 - * @category symbols + * @category Symbols */ export type InterruptedExceptionTypeId = typeof InterruptedExceptionTypeId /** + * A unique symbol identifying the `IllegalArgumentException` type. + * + * **Details** + * + * This provides a symbol that identifies an `IllegalArgumentException`. This is + * often used in scenarios where invalid arguments are supplied to methods that + * expect specific input. + * + * @see {@link IllegalArgumentException} Create or work with an `IllegalArgumentException` + * * @since 2.0.0 - * @category symbols + * @category Symbols */ export const IllegalArgumentExceptionTypeId: unique symbol = core.IllegalArgumentExceptionTypeId /** * @since 2.0.0 - * @category symbols + * @category Symbols */ export type IllegalArgumentExceptionTypeId = typeof IllegalArgumentExceptionTypeId /** + * A unique symbol identifying the `NoSuchElementException` type. + * + * **Details** + * + * This provides a symbol that identifies a `NoSuchElementException`. It helps + * differentiate cases where a required element is missing within a data + * structure. + * + * @see {@link NoSuchElementException} Create or work with a `NoSuchElementException` + * * @since 2.0.0 - * @category symbols + * @category Symbols */ export const NoSuchElementExceptionTypeId: unique symbol = core.NoSuchElementExceptionTypeId /** * @since 2.0.0 - * @category symbols + * @category Symbols */ export type NoSuchElementExceptionTypeId = typeof NoSuchElementExceptionTypeId /** + * A unique symbol identifying the `InvalidPubSubCapacityException` type. + * + * **Details** + * + * This provides a symbol that identifies an `InvalidPubSubCapacityException`. + * It indicates an error related to an invalid capacity passed to a `PubSub` + * structure. + * + * @see {@link InvalidPubSubCapacityException} Create or work with an `InvalidPubSubCapacityException` + * * @since 2.0.0 - * @category symbols + * @category Symbols */ export const InvalidPubSubCapacityExceptionTypeId: unique symbol = core.InvalidPubSubCapacityExceptionTypeId /** * @since 2.0.0 - * @category symbols + * @category Symbols */ export type InvalidPubSubCapacityExceptionTypeId = typeof InvalidPubSubCapacityExceptionTypeId /** + * A unique symbol identifying the `ExceededCapacityException` type. + * + * **Details** + * + * This provides a symbol that identifies an `ExceededCapacityException`. It + * denotes situations where a resource has exceeded its configured capacity + * limit. + * + * @see {@link ExceededCapacityException} Create or work with an `ExceededCapacityException` + * * @since 3.5.0 - * @category symbols + * @category Symbols */ export const ExceededCapacityExceptionTypeId: unique symbol = core.ExceededCapacityExceptionTypeId /** * @since 3.5.0 - * @category symbols + * @category Symbols */ export type ExceededCapacityExceptionTypeId = typeof ExceededCapacityExceptionTypeId /** + * A unique symbol identifying the `TimeoutException` type. + * + * **Details** + * + * This provides a symbol that identifies a `TimeoutException`. It helps the + * framework recognize errors related to operations that fail to complete within + * a given timeframe. + * + * @see {@link TimeoutException} Create or work with a `TimeoutException` + * * @since 2.0.0 - * @category symbols + * @category Symbols */ export const TimeoutExceptionTypeId: unique symbol = core.TimeoutExceptionTypeId /** * @since 2.0.0 - * @category symbols + * @category Symbols */ export type TimeoutExceptionTypeId = typeof TimeoutExceptionTypeId /** + * A unique symbol identifying the `UnknownException` type. + * + * **Details** + * + * This provides a symbol that identifies an `UnknownException`. It is typically + * used for generic or unexpected errors that do not fit other specific + * exception categories. + * + * @see {@link UnknownException} Create or work with an `UnknownException` + * * @since 2.0.0 - * @category symbols + * @category Symbols */ export const UnknownExceptionTypeId: unique symbol = core.UnknownExceptionTypeId /** * @since 2.0.0 - * @category symbols + * @category Symbols */ export type UnknownExceptionTypeId = typeof UnknownExceptionTypeId /** - * A `Cause` represents the full history of a failure resulting from running an - * `Effect` workflow. + * Represents the full history of a failure within an `Effect`. + * + * **Details** * - * Effect-TS uses a data structure from functional programming called a semiring - * to represent the `Cause` data type. This allows us to take a base type `E` - * (which represents the error type of an `Effect`) and capture the sequential - * and parallel composition of errors in a fully lossless fashion. + * This type is a data structure that captures all information about why and how + * an effect has failed, including parallel errors, sequential errors, defects, + * and interruptions. It enables a "lossless" error model: no error-related + * information is discarded, which helps in debugging and understanding the root + * cause of failures. * * @since 2.0.0 - * @category models + * @category Models */ export type Cause = | Empty @@ -172,8 +263,10 @@ export type Cause = */ export declare namespace Cause { /** + * This interface is used internally to manage the type variance of `Cause`. + * * @since 2.0.0 - * @category models + * @category Models */ export interface Variance { readonly [CauseTypeId]: { @@ -183,11 +276,21 @@ export declare namespace Cause { } /** - * Represents a set of methods that can be used to reduce a `Cause` to a - * specified value of type `Z` with access to a context of type `C`. + * Describes methods for reducing a `Cause` into a value of type `Z` with + * access to contextual information. + * + * **Details** + * + * This interface is meant for advanced transformations of `Cause`. By + * implementing each method, you can define how different parts of the `Cause` + * structure (like `Fail`, `Die`, or `Interrupt`) should be transformed into a + * final type `Z`. The `context` parameter carries additional data needed during + * this reduction. + * + * @see {@link reduceWithContext} Apply a `CauseReducer` to transform a `Cause` * * @since 2.0.0 - * @category models + * @category Models */ export interface CauseReducer { emptyCase(context: C): Z @@ -199,8 +302,10 @@ export interface CauseReducer { } /** + * Represents an error object that can be yielded in `Effect.gen`. + * * @since 2.0.0 - * @category models + * @category Models */ export interface YieldableError extends Pipeable, Inspectable, Readonly { readonly [Effect.EffectTypeId]: Effect.Effect.VarianceStruct @@ -211,18 +316,24 @@ export interface YieldableError extends Pipeable, Inspectable, Readonly { } /** - * Represents a generic checked exception which occurs at runtime. + * Creates an error that occurs at runtime, extendable for other exception + * types. * * @since 2.0.0 - * @category errors + * @category Errors */ export const YieldableError: new(message?: string | undefined) => YieldableError = core.YieldableError /** - * Represents a generic checked exception which occurs at runtime. + * An error representing a runtime error. + * + * **Details** + * + * This interface is used for errors that occur at runtime but are still + * considered recoverable or typed. * * @since 2.0.0 - * @category models + * @category Models */ export interface RuntimeException extends YieldableError { readonly _tag: "RuntimeException" @@ -230,10 +341,17 @@ export interface RuntimeException extends YieldableError { } /** - * Represents a checked exception which occurs when a `Fiber` is interrupted. + * An error representing fiber interruption. + * + * **Details** + * + * This interface represents errors that occur when a fiber is forcefully + * interrupted. Interruption can happen for various reasons, including + * cancellations or system directives to halt operations. Code that deals with + * concurrency might need to catch or handle these to ensure proper cleanup. * * @since 2.0.0 - * @category models + * @category Models */ export interface InterruptedException extends YieldableError { readonly _tag: "InterruptedException" @@ -241,11 +359,15 @@ export interface InterruptedException extends YieldableError { } /** - * Represents a checked exception which occurs when an invalid argument is - * provided to a method. + * An error representing an invalid argument passed to a method. + * + * **Details** + * + * This interface is used for signaling that a function or method received an + * argument that does not meet its preconditions. * * @since 2.0.0 - * @category models + * @category Models */ export interface IllegalArgumentException extends YieldableError { readonly _tag: "IllegalArgumentException" @@ -253,11 +375,16 @@ export interface IllegalArgumentException extends YieldableError { } /** - * Represents a checked exception which occurs when an expected element was - * unable to be found. + * An error that occurs when an expected element is missing. + * + * **Details** + * + * This interface indicates scenarios like looking up an item in a collection + * or searching for data that should be present but isn't. It helps your code + * signal a more specific issue rather than a general error. * * @since 2.0.0 - * @category models + * @category Models */ export interface NoSuchElementException extends YieldableError { readonly _tag: "NoSuchElementException" @@ -265,11 +392,10 @@ export interface NoSuchElementException extends YieldableError { } /** - * Represents a checked exception which occurs when attempting to construct a - * `PubSub` with an invalid capacity. + * An error indicating invalid capacity for a `PubSub`. * * @since 2.0.0 - * @category models + * @category Models */ export interface InvalidPubSubCapacityException extends YieldableError { readonly _tag: "InvalidPubSubCapacityException" @@ -277,11 +403,10 @@ export interface InvalidPubSubCapacityException extends YieldableError { } /** - * Represents a checked exception which occurs when a resources capacity has - * been exceeded. + * An error that occurs when resource capacity is exceeded. * * @since 3.5.0 - * @category models + * @category Models */ export interface ExceededCapacityException extends YieldableError { readonly _tag: "ExceededCapacityException" @@ -289,11 +414,10 @@ export interface ExceededCapacityException extends YieldableError { } /** - * Represents a checked exception which occurs when a computation doesn't - * finish on schedule. + * An error representing a computation that timed out. * * @since 2.0.0 - * @category models + * @category Models */ export interface TimeoutException extends YieldableError { readonly _tag: "TimeoutException" @@ -301,11 +425,16 @@ export interface TimeoutException extends YieldableError { } /** - * Represents a checked exception which occurs when an unknown error is thrown, such as - * from a rejected promise. + * A checked exception for handling unknown or unexpected errors. + * + * **Details** + * + * This interface captures errors that don't fall under known categories. It is + * especially helpful for wrapping low-level or third-party library errors that + * might provide little or no context, such as from a rejected promise. * * @since 2.0.0 - * @category models + * @category Models */ export interface UnknownException extends YieldableError { readonly _tag: "UnknownException" @@ -314,21 +443,32 @@ export interface UnknownException extends YieldableError { } /** - * The `Empty` cause represents a lack of errors. + * Represents a lack of errors within a `Cause`. + * + * @see {@link empty} Construct a new `Empty` cause + * @see {@link isEmptyType} Check if a `Cause` is an `Empty` type * * @since 2.0.0 - * @category models + * @category Models */ export interface Empty extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { readonly _tag: "Empty" } /** - * The `Fail` cause represents a `Cause` which failed with an expected error of - * type `E`. + * Represents an expected error within a `Cause`. + * + * **Details** + * + * This interface models a `Cause` that carries an expected or known error of + * type `E`. For example, if you validate user input and find it invalid, you + * might store that error within a `Fail`. + * + * @see {@link fail} Construct a `Fail` cause + * @see {@link isFailType} Check if a `Cause` is a `Fail` * * @since 2.0.0 - * @category models + * @category Models */ export interface Fail extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { readonly _tag: "Fail" @@ -336,12 +476,19 @@ export interface Fail extends Cause.Variance, Equal.Equal, Pipeable, I } /** - * The `Die` cause represents a `Cause` which failed as a result of a defect, or - * in other words, an unexpected error. + * Represents an unexpected defect within a `Cause`. + * + * **Details** + * + * This interface models a `Cause` for errors that are typically unrecoverable or + * unanticipated—like runtime exceptions or bugs. When code "dies," it indicates a + * severe failure that wasn't accounted for. + * + * @see {@link die} Construct a `Die` cause + * @see {@link isDieType} Check if a `Cause` is a `Die` * - * type `E`. * @since 2.0.0 - * @category models + * @category Models */ export interface Die extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { readonly _tag: "Die" @@ -349,11 +496,20 @@ export interface Die extends Cause.Variance, Equal.Equal, Pipeable, Inspe } /** - * The `Interrupt` cause represents failure due to `Fiber` interruption, which - * contains the `FiberId` of the interrupted `Fiber`. + * Represents fiber interruption within a `Cause`. + * + * **Details** + * + * This interface models a scenario where an effect was halted by an external + * signal, carrying a `FiberId` that identifies which fiber was interrupted. + * Interruption is a normal part of concurrency, used for cancellation or + * resource cleanup. + * + * @see {@link interrupt} Construct an `Interrupt` cause + * @see {@link isInterruptType} Check if a `Cause` is an `Interrupt` * * @since 2.0.0 - * @category models + * @category Models */ export interface Interrupt extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { readonly _tag: "Interrupt" @@ -361,17 +517,19 @@ export interface Interrupt extends Cause.Variance, Equal.Equal, Pipeable, } /** - * The `Parallel` cause represents the composition of two causes which occurred - * in parallel. + * Represents parallel composition of two `Cause`s. * - * In Effect-TS programs, it is possible that two operations may be performed in - * parallel. In these cases, the `Effect` workflow can fail for more than one - * reason. If both computations fail, then there are actually two errors which - * occurred in parallel. In these cases, the errors can be represented by the - * `Parallel` cause. + * **Details** + * + * This interface captures failures that happen simultaneously. In scenarios + * with concurrency, more than one operation can fail in parallel. Instead of + * losing information, this structure stores both errors together. + * + * @see {@link parallel} Combine two `Cause`s in parallel + * @see {@link isParallelType} Check if a `Cause` is a `Parallel` * * @since 2.0.0 - * @category models + * @category Models */ export interface Parallel extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { readonly _tag: "Parallel" @@ -380,16 +538,19 @@ export interface Parallel extends Cause.Variance, Equal.Equal, Pipeabl } /** - * The `Sequential` cause represents the composition of two causes which occurred - * sequentially. + * Represents sequential composition of two `Cause`s. * - * For example, if we perform Effect-TS's analog of `try-finally` (i.e. - * `Effect.ensuring`), and both the `try` and `finally` blocks fail, we have two - * errors which occurred sequentially. In these cases, the errors can be - * represented by the `Sequential` cause. + * **Details** + * + * This interface models the scenario where one error follows another in + * sequence, such as when a main effect fails and then a finalizer also fails. + * It ensures both errors are retained in the final `Cause`. + * + * @see {@link sequential} Combine two `Cause`s sequentially + * @see {@link isSequentialType} Check if a `Cause` is a `Sequential` * * @since 2.0.0 - * @category models + * @category Models */ export interface Sequential extends Cause.Variance, Equal.Equal, Pipeable, Inspectable { readonly _tag: "Sequential" @@ -398,274 +559,421 @@ export interface Sequential extends Cause.Variance, Equal.Equal, Pipea } /** - * Constructs a new `Empty` cause. + * Creates an `Empty` cause. + * + * **Details** + * + * This function returns a cause that signifies "no error." It's commonly used + * to represent an absence of failure conditions. + * + * @see {@link isEmpty} Check if a `Cause` is empty * * @since 2.0.0 - * @category constructors + * @category Constructors */ export const empty: Cause = internal.empty /** - * Constructs a new `Fail` cause from the specified `error`. + * Creates a `Fail` cause from an expected error. + * + * **Details** + * + * This function constructs a `Cause` carrying an error of type `E`. It's used + * when you want to represent a known or anticipated failure in your effectful + * computations. + * + * @see {@link isFailure} Check if a `Cause` contains a failure * * @since 2.0.0 - * @category constructors + * @category Constructors */ export const fail: (error: E) => Cause = internal.fail /** - * Constructs a new `Die` cause from the specified `defect`. + * Creates a `Die` cause from an unexpected error. + * + * **Details** + * + * This function wraps an unhandled or unknown defect (like a runtime crash) + * into a `Cause`. It's useful for capturing unforeseen issues in a structured + * way. + * + * @see {@link isDie} Check if a `Cause` contains a defect * * @since 2.0.0 - * @category constructors + * @category Constructors */ export const die: (defect: unknown) => Cause = internal.die /** - * Constructs a new `Interrupt` cause from the specified `fiberId`. + * Creates an `Interrupt` cause from a `FiberId`. + * + * **Details** + * + * This function represents a fiber that has been interrupted. It stores the + * identifier of the interrupted fiber, enabling precise tracking of concurrent + * cancellations. + * + * @see {@link isInterrupted} Check if a `Cause` contains an interruption * * @since 2.0.0 - * @category constructors + * @category Constructors */ export const interrupt: (fiberId: FiberId.FiberId) => Cause = internal.interrupt /** - * Constructs a new `Parallel` cause from the specified `left` and `right` - * causes. + * Combines two `Cause`s in parallel. + * + * **Details** + * + * This function merges two errors that occurred simultaneously. Instead of + * discarding one error, both are retained, allowing for richer error reporting + * and debugging. + * + * @see {@link isParallelType} Check if a `Cause` is a `Parallel` * * @since 2.0.0 - * @category constructors + * @category Constructors */ export const parallel: (left: Cause, right: Cause) => Cause = internal.parallel /** - * Constructs a new `Sequential` cause from the specified pecified `left` and - * `right` causes. + * Combines two `Cause`s sequentially. + * + * **Details** + * + * This function merges two errors that occurred in sequence, such as a main + * error followed by a finalization error. It preserves both errors for complete + * failure information. + * + * @see {@link isSequentialType} Check if a `Cause` is a `Sequential` * * @since 2.0.0 - * @category constructors + * @category Constructors */ export const sequential: (left: Cause, right: Cause) => Cause = internal.sequential /** - * Returns `true` if the specified value is a `Cause`, `false` otherwise. + * Checks if a value is a `Cause`. * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isCause: (u: unknown) => u is Cause = internal.isCause /** - * Returns `true` if the specified `Cause` is an `Empty` type, `false` - * otherwise. + * Checks if a `Cause` is an `Empty` type. + * + * @see {@link empty} Create a new `Empty` cause * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isEmptyType: (self: Cause) => self is Empty = internal.isEmptyType /** - * Returns `true` if the specified `Cause` is a `Fail` type, `false` - * otherwise. + * Checks if a `Cause` is a `Fail` type. + * + * @see {@link fail} Create a new `Fail` cause * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isFailType: (self: Cause) => self is Fail = internal.isFailType /** - * Returns `true` if the specified `Cause` is a `Die` type, `false` - * otherwise. + * Checks if a `Cause` is a `Die` type. + * + * @see {@link die} Create a new `Die` cause * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isDieType: (self: Cause) => self is Die = internal.isDieType /** - * Returns `true` if the specified `Cause` is an `Interrupt` type, `false` - * otherwise. + * Checks if a `Cause` is an `Interrupt` type. + * + * @see {@link interrupt} Create an `Interrupt` cause * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isInterruptType: (self: Cause) => self is Interrupt = internal.isInterruptType /** - * Returns `true` if the specified `Cause` is a `Sequential` type, `false` - * otherwise. + * Checks if a `Cause` is a `Sequential` type. + * + * @see {@link sequential} Combine two `Cause`s sequentially * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isSequentialType: (self: Cause) => self is Sequential = internal.isSequentialType /** - * Returns `true` if the specified `Cause` is a `Parallel` type, `false` - * otherwise. + * Checks if a `Cause` is a `Parallel` type. + * + * @see {@link parallel} Combine two `Cause`s in parallel * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isParallelType: (self: Cause) => self is Parallel = internal.isParallelType /** - * Returns the size of the cause, calculated as the number of individual `Cause` - * nodes found in the `Cause` semiring structure. + * Calculates the size of a `Cause`. + * + * **Details** + * + * This function returns the total number of `Cause` nodes in the semiring + * structure, reflecting how many individual error elements are recorded. * * @since 2.0.0 - * @category getters + * @category Getters */ export const size: (self: Cause) => number = internal.size /** - * Returns `true` if the specified cause is empty, `false` otherwise. + * Checks if a `Cause` is entirely empty. + * + * **Details** + * + * This function returns `true` if the `Cause` contains no errors, defects, or + * interruptions. It's helpful for verifying if a computation truly had no + * failures. * * @since 2.0.0 - * @category getters + * @category Getters */ export const isEmpty: (self: Cause) => boolean = internal.isEmpty /** - * Returns `true` if the specified cause contains a failure, `false` otherwise. + * Checks if a `Cause` contains a failure. + * + * **Details** + * + * This function returns `true` if the `Cause` includes any `Fail` error. It's + * commonly used to confirm whether a workflow encountered an anticipated error + * versus just defects or interruptions. * * @since 2.0.0 - * @category getters + * @category Getters */ export const isFailure: (self: Cause) => boolean = internal.isFailure /** - * Returns `true` if the specified cause contains a defect, `false` otherwise. + * Checks if a `Cause` contains a defect. + * + * **Details** + * + * This function returns `true` if the `Cause` includes any unexpected or + * unhandled errors (`Die`). It's useful for differentiating known failures from + * unexpected ones. * * @since 2.0.0 - * @category getters + * @category Getters */ export const isDie: (self: Cause) => boolean = internal.isDie /** - * Returns `true` if the specified cause contains an interruption, `false` - * otherwise. + * Checks if a `Cause` contains an interruption. + * + * **Details** + * + * This function returns `true` if the `Cause` includes any fiber interruptions. * * @since 2.0.0 - * @category getters + * @category Getters */ export const isInterrupted: (self: Cause) => boolean = internal.isInterrupted /** - * Returns `true` if the specified cause contains only interruptions (without - * any `Die` or `Fail` causes), `false` otherwise. + * Checks if a `Cause` contains only interruptions. + * + * **Details** + * + * This function returns `true` if the `Cause` has been interrupted but does not + * contain any other failures, such as `Fail` or `Die`. It's helpful for + * verifying purely "cancellation" scenarios. * * @since 2.0.0 - * @category getters + * @category Getters */ export const isInterruptedOnly: (self: Cause) => boolean = internal.isInterruptedOnly /** - * Returns a `List` of all recoverable errors of type `E` in the specified - * cause. + * Extracts all recoverable errors of type `E` from a `Cause`. + * + * **Details** + * + * This function returns a chunk of errors, providing a list of all `Fail` + * values found in the cause. It's useful for collecting all known failures for + * logging or combined error handling. * * @since 2.0.0 - * @category getters + * @category Getters */ export const failures: (self: Cause) => Chunk.Chunk = internal.failures /** - * Returns a `List` of all unrecoverable defects in the specified cause. + * Extracts all unrecoverable defects from a `Cause`. + * + * **Details** + * + * This function returns a chunk of values representing unexpected errors + * (`Die`). It's handy for capturing or logging unanticipated failures that + * might need special handling, such as bug reports. * * @since 2.0.0 - * @category getters + * @category Getters */ export const defects: (self: Cause) => Chunk.Chunk = internal.defects /** - * Returns a `HashSet` of `FiberId`s for all fibers that interrupted the fiber - * described by the specified cause. + * Collects all `FiberId`s responsible for interrupting a fiber. + * + * **Details** + * + * This function returns a set of IDs indicating which fibers caused + * interruptions within this `Cause`. It's useful for debugging concurrency + * issues or tracing cancellations. * * @since 2.0.0 - * @category getters + * @category Getters */ export const interruptors: (self: Cause) => HashSet.HashSet = internal.interruptors /** - * Returns the `E` associated with the first `Fail` in this `Cause`, if one - * exists. + * Retrieves the first `Fail` error in a `Cause`, if present. + * + * **Details** + * + * This function returns an `Option` containing the first recoverable error + * (`E`) from the cause. It's often used to quickly check if there's a primary + * error to handle or display. * * @since 2.0.0 - * @category getters + * @category Getters */ export const failureOption: (self: Cause) => Option.Option = internal.failureOption /** - * Returns the first checked error on the `Left` if available, if there are - * no checked errors return the rest of the `Cause` that is known to contain - * only `Die` or `Interrupt` causes. + * Splits a `Cause` into either its first `Fail` error or the rest of the cause + * (which might only contain `Die` or `Interrupt`). + * + * **Details** + * + * This function either returns the checked error (`E`) or the remaining + * `Cause` with defects/interruptions. It helps you decide if there's a + * recoverable path or if only unhandled issues remain. * * @since 2.0.0 - * @category getters + * @category Getters */ export const failureOrCause: (self: Cause) => Either.Either, E> = internal.failureOrCause /** - * Converts the specified `Cause>` to an `Option>` by - * recursively stripping out any failures with the error `None`. + * Strips out failures with an error of `None` from a `Cause>`. + * + * **Details** + * + * This function turns a `Cause>` into an `Option>`. If the + * cause only contains failures of `None`, it becomes `None`; otherwise, it + * returns a `Cause` of the remaining errors. It's helpful when working with + * optional errors and filtering out certain error paths. * * @since 2.0.0 - * @category getters + * @category Getters */ export const flipCauseOption: (self: Cause>) => Option.Option> = internal.flipCauseOption /** - * Returns the defect associated with the first `Die` in this `Cause`, if one - * exists. + * Retrieves the first `Die` defect in a `Cause`, if present. + * + * **Details** + * + * This function returns an `Option` containing the first unexpected failure + * (`Die`) discovered. It's helpful for diagnosing the primary defect in a chain + * of errors. * * @since 2.0.0 - * @category getters + * @category Getters */ export const dieOption: (self: Cause) => Option.Option = internal.dieOption /** - * Returns the `FiberId` associated with the first `Interrupt` in the specified - * cause, if one exists. + * Retrieves the first `Interrupt` in a `Cause`, if present. + * + * **Details** + * + * This function returns an `Option` with the first fiber interruption + * discovered. This is particularly useful for concurrency analysis or debugging + * cancellations. * * @since 2.0.0 - * @category getters + * @category Getters */ export const interruptOption: (self: Cause) => Option.Option = internal.interruptOption /** - * Remove all `Fail` and `Interrupt` nodes from the specified cause, and return - * a cause containing only `Die` cause/finalizer defects. + * Removes all `Fail` and `Interrupt` nodes, keeping only defects (`Die`) in a + * `Cause`. + * + * **Details** + * + * This function strips a cause of recoverable errors and interruptions, leaving + * only unexpected failures. If no defects remain, it returns `None`. It's + * valuable for focusing only on unanticipated problems when both known errors + * and defects could occur. * * @since 2.0.0 - * @category getters + * @category Getters */ export const keepDefects: (self: Cause) => Option.Option> = internal.keepDefects /** - * Linearizes the specified cause into a `HashSet` of parallel causes where each - * parallel cause contains a linear sequence of failures. + * Linearizes a `Cause` into a set of parallel causes, each containing a + * sequential chain of failures. + * + * **Details** + * + * This function reorganizes the cause structure so that you can analyze each + * parallel branch separately, even if they have multiple sequential errors. * * @since 2.0.0 - * @category getters + * @category Getters */ export const linearize: (self: Cause) => HashSet.HashSet> = internal.linearize /** - * Remove all `Fail` and `Interrupt` nodes from the specified cause, and return - * a cause containing only `Die` cause/finalizer defects. + * Removes `Fail` and `Interrupt` nodes from a `Cause`, keeping only defects + * (`Die`). + * + * **Details** + * + * This function is similar to `keepDefects` but returns a `Cause` + * directly, which can still store `Die` or finalizer-related defects. It's + * helpful for analyzing only the irrecoverable portion of the error. * * @since 2.0.0 - * @category getters + * @category Getters */ export const stripFailures: (self: Cause) => Cause = internal.stripFailures /** - * Remove all `Die` causes that the specified partial function is defined at, - * returning `Some` with the remaining causes or `None` if there are no - * remaining causes. + * Removes matching defects from a `Cause` using a partial function, returning + * the remainder. + * + * **Details** + * + * This function applies a user-defined extraction function to each defect + * (`Die`). If the function matches the defect, that defect is removed. If all + * defects match, the result is `None`. Otherwise, you get a `Cause` with the + * unmatched defects. * * @since 2.0.0 - * @category getters + * @category Getters */ export const stripSomeDefects: { (pf: (defect: unknown) => Option.Option): (self: Cause) => Option.Option> @@ -673,8 +981,18 @@ export const stripSomeDefects: { } = internal.stripSomeDefects /** + * Replaces any errors in a `Cause` with a provided constant error. + * + * **Details** + * + * This function transforms all `Fail` errors into the specified error value, + * preserving the structure of the `Cause`. It's useful when you no longer need + * the original error details but still want to keep the cause shape. + * + * @see {@link map} Apply a custom transformation to `Fail` errors + * * @since 2.0.0 - * @category mapping + * @category Mapping */ export const as: { (error: E2): (self: Cause) => Cause @@ -682,8 +1000,18 @@ export const as: { } = internal.as /** + * Transforms the errors in a `Cause` using a user-provided function. + * + * **Details** + * + * This function applies `f` to each `Fail` error while leaving defects (`Die`) + * and interruptions untouched. It's useful for changing or simplifying error + * types in your effectful workflows. + * + * @see {@link as} Replace errors with a single constant + * * @since 2.0.0 - * @category mapping + * @category Mapping */ export const map: { (f: (e: E) => E2): (self: Cause) => Cause @@ -691,8 +1019,18 @@ export const map: { } = internal.map /** + * Transforms errors in a `Cause` into new causes. + * + * **Details** + * + * This function applies a function `f` to each `Fail` error, converting it into + * a new `Cause`. This is especially powerful for merging or restructuring error + * types while preserving or combining cause information. + * + * @see {@link map} Apply a simpler transformation to errors + * * @since 2.0.0 - * @category sequencing + * @category Sequencing */ export const flatMap: { (f: (e: E) => Cause): (self: Cause) => Cause @@ -700,10 +1038,11 @@ export const flatMap: { } = internal.flatMap /** - * Executes a sequence of two `Cause`s. The second `Cause` can be dependent on the result of the first `Cause`. + * Sequences two `Cause`s. The second `Cause` can be dependent on the result of + * the first `Cause`. * * @since 2.0.0 - * @category sequencing + * @category Sequencing */ export const andThen: { (f: (e: E) => Cause): (self: Cause) => Cause @@ -713,17 +1052,32 @@ export const andThen: { } = internal.andThen /** + * Flattens a nested `Cause` structure. + * + * **Details** + * + * This function takes a `Cause>` and merges the layers into a single + * `Cause`. It's useful for eliminating additional nesting created by + * repeated transformations or compositions. + * + * @see {@link flatMap} Compose nested causes + * * @since 2.0.0 - * @category sequencing + * @category Sequencing */ export const flatten: (self: Cause>) => Cause = internal.flatten /** - * Returns `true` if the `self` cause contains or is equal to `that` cause, - * `false` otherwise. + * Checks if the current `Cause` contains or is equal to another `Cause`. + * + * **Details** + * + * This function returns `true` if `that` cause is part of or the same as + * the current `Cause`. It's useful when you need to check for specific + * error patterns or deduplicate repeated failures. * * @since 2.0.0 - * @category elements + * @category Elements */ export const contains: { (that: Cause): (self: Cause) => boolean @@ -731,21 +1085,61 @@ export const contains: { } = internal.contains /** - * Squashes a `Cause` down to a single defect, chosen to be the "most important" - * defect. + * Extracts the most "important" defect from a `Cause`. + * + * **Details** + * + * This function reduces a `Cause` to a single, prioritized defect. It evaluates + * the `Cause` in the following order of priority: + * + * 1. If the `Cause` contains a failure (e.g., from `Effect.fail`), it returns + * the raw error value. + * 2. If there is no failure, it looks for the first defect (e.g., from + * `Effect.die`). + * 3. If neither of the above is present, and the `Cause` stems from an + * interruption, it creates and returns an `InterruptedException`. + * + * This function ensures you can always extract a meaningful representation of + * the primary issue from a potentially complex `Cause` structure. + * + * **When to Use** + * + * Use this function when you need to extract the most relevant error or defect + * from a `Cause`, especially in scenarios where multiple errors or defects may + * be present. It's particularly useful for simplifying error reporting or + * logging. + * + * @see {@link squashWith} Allows transforming failures into defects when squashing. * * @since 2.0.0 - * @category destructors + * @category Destructors */ export const squash: (self: Cause) => unknown = core.causeSquash /** - * Squashes a `Cause` down to a single defect, chosen to be the "most important" - * defect. If a recoverable error is found, the provided function will be used - * to map the error a defect, and the resulting value will be returned. + * Extracts the most "important" defect from a `Cause`, transforming failures + * into defects using a provided function. + * + * **Details** + * + * This function reduces a `Cause` to a single, prioritized defect, while + * allowing you to transform recoverable failures into defects through a custom + * function. It processes the `Cause` in the following order: + * + * 1. If the `Cause` contains a failure (e.g., from `Effect.fail`), it applies + * the provided function `f` to the error to transform it into a defect. + * 2. If there is no failure, it looks for the first defect (e.g., from + * `Effect.die`) and returns it. + * 3. If neither is present and the `Cause` stems from an interruption, it + * returns an `InterruptedException`. + * + * This function is particularly useful when you need custom handling or + * transformation of errors while processing a `Cause`. + * + * @see {@link squash} Extracts the most "important" defect without transforming failures. * * @since 2.0.0 - * @category destructors + * @category Destructors */ export const squashWith: { (f: (error: E) => unknown): (self: Cause) => unknown @@ -753,11 +1147,25 @@ export const squashWith: { } = core.causeSquashWith /** - * Uses the provided partial function to search the specified cause and attempt - * to extract information from it. + * Searches a `Cause` using a partial function to extract information. + * + * **Details** + * + * This function allows you to search through a `Cause` using a custom partial + * function. The partial function is applied to the `Cause`, and if it matches, + * the result is returned wrapped in a `Some`. If no match is found, the result + * is `None`. + * + * This is particularly useful when you are only interested in specific types of + * errors, defects, or interruption causes within a potentially complex `Cause` + * structure. By leveraging a partial function, you can focus on extracting only + * the relevant information you care about. + * + * The partial function should return an `Option` indicating whether it matched + * and the value it extracted. * * @since 2.0.0 - * @category elements + * @category Elements */ export const find: { (pf: (cause: Cause) => Option.Option): (self: Cause) => Option.Option @@ -765,10 +1173,25 @@ export const find: { } = internal.find /** - * Filters causes which match the provided predicate out of the specified cause. + * Preserves parts of a `Cause` that match a given predicate. + * + * **Details** + * + * This function allows you to retain only the parts of a `Cause` structure that + * match a specified predicate or refinement. Any parts of the `Cause` that do + * not match the provided condition are excluded from the result. + * + * You can use this function in two ways: + * - With a `Predicate`: A function that evaluates whether a `Cause` should be + * retained based on its value. + * - With a `Refinement`: A more specific predicate that can refine the type of + * the `Cause`. + * + * This is useful when you need to extract specific types of errors, defects, or + * interruptions from a `Cause` while discarding unrelated parts. * * @since 2.0.0 - * @category filtering + * @category Filtering */ export const filter: { (refinement: Refinement>, Cause>): (self: Cause) => Cause @@ -778,10 +1201,31 @@ export const filter: { } = internal.filter /** - * Folds the specified cause into a value of type `Z`. + * Transforms a `Cause` into a single value using custom handlers for each + * possible case. + * + * **Details** + * + * This function processes a `Cause` by applying a set of custom handlers to + * each possible type of cause: `Empty`, `Fail`, `Die`, `Interrupt`, + * `Sequential`, and `Parallel`. The result of this function is a single value + * of type `Z`. This function allows you to define exactly how to handle each + * part of a `Cause`, whether it's a failure, defect, interruption, or a + * combination of these. + * + * The options parameter provides handlers for: + * - `onEmpty`: Handles the case where the cause is `Empty`, meaning no errors + * occurred. + * - `onFail`: Processes a failure with an error of type `E`. + * - `onDie`: Processes a defect (unexpected error). + * - `onInterrupt`: Handles a fiber interruption, providing the `FiberId` of the + * interruption. + * - `onSequential`: Combines two sequential causes into a single value of type + * `Z`. + * - `onParallel`: Combines two parallel causes into a single value of type `Z`. * * @since 2.0.0 - * @category folding + * @category Matching */ export const match: { ( @@ -808,11 +1252,29 @@ export const match: { } = internal.match /** - * Reduces the specified cause into a value of type `Z`, beginning with the - * provided `zero` value. + * Combines all parts of a `Cause` into a single value by starting with an + * initial value. + * + * **Details** + * + * This function processes a `Cause` by starting with an initial value (`zero`) + * and applying a custom function (`pf`) to combine all elements of the `Cause` + * into a single result of type `Z`. The custom function determines how each + * part of the `Cause` contributes to the final result. The function can return + * an `Option` to either continue combining values or skip specific parts of the + * `Cause`. + * + * This function is useful for tasks such as: + * - Aggregating error messages from a `Cause` into a single string. + * - Summarizing the structure of a `Cause` into a simplified result. + * - Filtering or processing only specific parts of a `Cause`. + * + * The reduction proceeds in a top-down manner, visiting all nodes in the + * `Cause` structure. This gives you complete control over how each part of the + * `Cause` contributes to the final result. * * @since 2.0.0 - * @category folding + * @category Reducing */ export const reduce: { (zero: Z, pf: (accumulator: Z, cause: Cause) => Option.Option): (self: Cause) => Z @@ -820,11 +1282,27 @@ export const reduce: { } = internal.reduce /** - * Reduces the specified cause into a value of type `Z` using a `Cause.Reducer`. - * Also allows for accessing the provided context during reduction. + * Combines all parts of a `Cause` into a single value using a custom reducer + * and a context. + * + * **Details** + * + * This function allows you to reduce a `Cause` into a single value of type `Z` + * using a custom `CauseReducer`. A `CauseReducer` provides methods to handle + * specific parts of the `Cause`, such as failures, defects, or interruptions. + * Additionally, this function provides access to a `context` value, which can + * be used to carry information or maintain state during the reduction process. + * + * This is particularly useful when the reduction process needs additional + * context or configuration, such as: + * - Aggregating error details with dynamic formatting. + * - Collecting logs or statistics about the `Cause`. + * - Performing stateful transformations based on the `context`. + * + * @see {@link reduce} To reduce a `Cause` without additional context. * * @since 2.0.0 - * @category folding + * @category Reducing */ export const reduceWithContext: { (context: C, reducer: CauseReducer): (self: Cause) => Z @@ -832,83 +1310,108 @@ export const reduceWithContext: { } = internal.reduceWithContext /** - * Represents a checked exception which occurs when a `Fiber` is interrupted. + * Creates an error that indicates a `Fiber` was interrupted. + * + * **Details** + * + * This function constructs an `InterruptedException` recognized by the Effect + * runtime. It is usually thrown or returned when a fiber's execution is + * interrupted by external events or by another fiber. This is particularly + * helpful in concurrent programs where fibers may halt each other before + * completion. * * @since 2.0.0 - * @category errors + * @category Errors */ export const InterruptedException: new(message?: string | undefined) => InterruptedException = core.InterruptedException /** - * Returns `true` if the specified value is an `InterruptedException`, `false` - * otherwise. + * Checks if a given unknown value is an `InterruptedException`. * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isInterruptedException: (u: unknown) => u is InterruptedException = core.isInterruptedException /** - * Represents a checked exception which occurs when an invalid argument is - * provided to a method. + * Creates an error indicating an invalid method argument. + * + * **Details** + * + * This function constructs an `IllegalArgumentException`. It is typically + * thrown or returned when an operation receives improper inputs, such as + * out-of-range values or invalid object states. * * @since 2.0.0 - * @category errors + * @category Errors */ export const IllegalArgumentException: new(message?: string | undefined) => IllegalArgumentException = core.IllegalArgumentException /** - * Returns `true` if the specified value is an `IllegalArgumentException`, `false` - * otherwise. + * Checks if a given unknown value is an `IllegalArgumentException`. * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isIllegalArgumentException: (u: unknown) => u is IllegalArgumentException = core.isIllegalArgumentException /** - * Represents a checked exception which occurs when an expected element was - * unable to be found. + * Creates an error indicating a missing element. + * + * **Details** + * + * This function constructs a `NoSuchElementException`. It helps you clearly + * communicate that a required element is unavailable. * * @since 2.0.0 - * @category errors + * @category Errors */ export const NoSuchElementException: new(message?: string | undefined) => NoSuchElementException = core.NoSuchElementException /** - * Returns `true` if the specified value is an `NoSuchElementException`, `false` - * otherwise. + * Checks if a given unknown value is a `NoSuchElementException`. * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isNoSuchElementException: (u: unknown) => u is NoSuchElementException = core.isNoSuchElementException /** - * Represents a generic checked exception which occurs at runtime. + * Creates an error for general runtime errors. + * + * **Details** + * + * This function constructs a `RuntimeException`, for errors that occur at + * runtime but are not specifically typed or categorized as interruptions, + * missing elements, or invalid arguments. It helps unify a wide range of + * unexpected conditions under a single, recognizable error type. * * @since 2.0.0 - * @category errors + * @category Errors */ export const RuntimeException: new(message?: string | undefined) => RuntimeException = core.RuntimeException /** - * Returns `true` if the specified value is an `RuntimeException`, `false` - * otherwise. + * Checks if a given unknown value is a `RuntimeException`. * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isRuntimeException: (u: unknown) => u is RuntimeException = core.isRuntimeException /** - * Represents a checked exception which occurs when a computation doesn't - * finish on schedule. + * Creates an error for operations that exceed their expected time. + * + * **Details** + * + * This function constructs a `TimeoutException`. It is typically used to signal + * that an operation or fiber did not complete within a designated time limit, + * allowing you to handle slow or hanging processes. * * @since 2.0.0 - * @category errors + * @category Errors */ export const TimeoutException: new(message?: string | undefined) => TimeoutException = core.TimeoutException @@ -941,70 +1444,102 @@ export const TimeoutException: new(message?: string | undefined) => TimeoutExcep * promises or external APIs. * * @since 2.0.0 - * @category errors + * @category Errors */ export const UnknownException: new(error: unknown, message?: string | undefined) => UnknownException = core.UnknownException /** - * Returns `true` if the specified value is an `UnknownException`, `false` - * otherwise. + * Checks if a given unknown value is an `UnknownException`. * * @since 2.0.0 - * @category refinements + * @category Guards */ export const isUnknownException: (u: unknown) => u is UnknownException = core.isUnknownException /** - * Represents a checked exception which occurs when a resources capacity has - * been exceeded. + * Creates an error indicating resource capacity has been exceeded. + * + * **Details** + * + * This function constructs an `ExceededCapacityException`, signifying that an + * operation or resource usage surpassed established limits. This can be + * essential for concurrency or resource management situations, ensuring your + * application doesn't go beyond acceptable thresholds. * * @since 3.5.0 - * @category errors + * @category Errors */ export const ExceededCapacityException: new(message?: string | undefined) => ExceededCapacityException = core.ExceededCapacityException /** - * Returns `true` if the specified value is an `ExceededCapacityException`, `false` - * otherwise. + * Checks if a given unknown value is an `ExceededCapacityException`. * * @since 3.5.0 - * @category refinements + * @category Guards */ export const isExceededCapacityException: (u: unknown) => u is ExceededCapacityException = core.isExceededCapacityException /** - * Returns the specified `Cause` as a pretty-printed string. + * Converts a `Cause` into a human-readable string. + * + * **Details** + * + * This function pretty-prints the entire `Cause`, including any failures, + * defects, and interruptions. It can be especially helpful for logging, + * debugging, or displaying structured errors to users. + * + * You can optionally pass `options` to configure how the error cause is + * rendered. By default, it includes essential details of all errors in the + * `Cause`. + * + * @see {@link prettyErrors} Get a list of `PrettyError` objects instead of a single string. * * @since 2.0.0 - * @category rendering + * @category Formatting */ export const pretty: (cause: Cause, options?: { readonly renderErrorCause?: boolean | undefined }) => string = internal.pretty /** + * A shape for prettified errors, optionally including a source span. + * * @since 3.2.0 - * @category models + * @category Models */ export interface PrettyError extends Error { readonly span: Span | undefined } /** - * Returns the specified `Cause` as a pretty-printed string. + * Returns a list of prettified errors (`PrettyError`) from a `Cause`. + * + * **Details** + * + * This function inspects the entire `Cause` and produces an array of + * `PrettyError` objects. Each object may include additional metadata, such as a + * `Span`, to provide deeper insights into where and how the error occurred. * * @since 3.2.0 - * @category rendering + * @category Formatting */ export const prettyErrors: (cause: Cause) => Array = internal.prettyErrors /** - * Returns the original, unproxied, instance of a thrown error + * Retrieves the original, unproxied error instance from an error object. + * + * **Details** + * + * This function returns the underlying error object without any + * library-specific wrapping or proxying that might occur during error handling. + * This can be essential if you need direct access to the error's native + * properties, such as stack traces or custom data fields, for detailed + * debugging or integration with external systems. * * @since 2.0.0 - * @category errors + * @category Errors */ export const originalError: (obj: E) => E = core.originalInstance diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index ff838159b3f..200b7fa94bc 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -996,7 +996,8 @@ const renderErrorCause = (cause: PrettyError, prefix: string) => { return stack } -class PrettyError extends globalThis.Error implements Cause.PrettyError { +/** @internal */ +export class PrettyError extends globalThis.Error implements Cause.PrettyError { span: undefined | Span = undefined constructor(originalError: unknown) { const originalErrorIsObject = typeof originalError === "object" && originalError !== null diff --git a/packages/effect/test/Cause.test.ts b/packages/effect/test/Cause.test.ts index c82d6df7b73..b0aa26eca70 100644 --- a/packages/effect/test/Cause.test.ts +++ b/packages/effect/test/Cause.test.ts @@ -1,15 +1,19 @@ -import * as Cause from "effect/Cause" -import * as Equal from "effect/Equal" +import * as assert from "assert" +import { Array as Arr, Cause, Effect, Either, Equal, FiberId, Hash, Option, Predicate } from "effect" import * as fc from "effect/FastCheck" -import * as FiberId from "effect/FiberId" -import * as Hash from "effect/Hash" +import { NodeInspectSymbol } from "effect/Inspectable" import * as internal from "effect/internal/cause" -import * as Option from "effect/Option" -import * as Predicate from "effect/Predicate" import { causes, equalCauses, errorCauseFunctions, errors } from "effect/test/utils/cause" -import { assert, describe, expect, it } from "vitest" +import { describe, expect, it } from "vitest" describe("Cause", () => { + const empty = Cause.empty + const failure = Cause.fail("error") + const defect = Cause.die("defect") + const interruption = Cause.interrupt(FiberId.runtime(1, 0)) + const sequential = Cause.sequential(failure, defect) + const parallel = Cause.parallel(failure, defect) + describe("InterruptedException", () => { it("correctly implements toString() and the NodeInspectSymbol", () => { // Referenced line to be included in the string output @@ -20,26 +24,54 @@ describe("Cause", () => { if (typeof window === "undefined") { // eslint-disable-next-line @typescript-eslint/no-var-requires const { inspect } = require("node:util") - expect(inspect(ex)).include("Cause.test.ts:16") // <= reference to the line above + expect(inspect(ex)).include("Cause.test.ts:20") // <= reference to the line above } }) }) + describe("UnknownException", () => { + it("exposes its `error` property", () => { + const err1 = new Cause.UnknownException("my message") + expect(err1.error).toBe("my message") + const err2 = new Cause.UnknownException(new Error("my error")) + expect(err2.error).toBeInstanceOf(Error) + expect((err2.error as Error).message).toBe("my error") + }) + + it("exposes its `cause` property", () => { + const err1 = new Cause.UnknownException("my message") + expect(err1.cause).toBe("my message") + const err2 = new Cause.UnknownException(new Error("my error")) + expect(err2.cause).toBeInstanceOf(Error) + expect((err2.cause as Error).message).toBe("my error") + }) + + it("uses a default message when none is provided", () => { + const err1 = new Cause.UnknownException("my message") + expect(err1.message).toBe("An unknown error occurred") + }) + + it("accepts a custom override message", () => { + const err1 = new Cause.UnknownException(new Error("my error"), "my message") + expect(err1.message).toBe("my message") + }) + }) + it("[internal] prettyErrorMessage converts errors into readable JSON-like strings", () => { class Error1 { readonly _tag = "WithTag" } - expect(internal.prettyErrorMessage(new Error1())).toEqual(`{"_tag":"WithTag"}`) + expect(internal.prettyErrorMessage(new Error1())).toBe(`{"_tag":"WithTag"}`) class Error2 { readonly _tag = "WithMessage" readonly message = "my message" } - expect(internal.prettyErrorMessage(new Error2())).toEqual(`{"_tag":"WithMessage","message":"my message"}`) + expect(internal.prettyErrorMessage(new Error2())).toBe(`{"_tag":"WithMessage","message":"my message"}`) class Error3 { readonly _tag = "WithName" readonly name = "my name" } - expect(internal.prettyErrorMessage(new Error3())).toEqual( + expect(internal.prettyErrorMessage(new Error3())).toBe( `{"_tag":"WithName","name":"my name"}` ) class Error4 { @@ -47,7 +79,7 @@ describe("Cause", () => { readonly name = "my name" readonly message = "my message" } - expect(internal.prettyErrorMessage(new Error4())).toEqual( + expect(internal.prettyErrorMessage(new Error4())).toBe( `{"_tag":"WithName","name":"my name","message":"my message"}` ) class Error5 { @@ -56,302 +88,602 @@ describe("Cause", () => { return "Error: my string" } } - expect(internal.prettyErrorMessage(new Error5())).toEqual( + expect(internal.prettyErrorMessage(new Error5())).toBe( `Error: my string` ) }) - describe("pretty", () => { - it("handles array-based errors without throwing", () => { - assert.strictEqual(Cause.pretty(Cause.fail([{ toString: "" }])), `Error: [{"toString":""}]`) - }) - - it("Empty", () => { - expect(Cause.pretty(Cause.empty)).toEqual("All fibers interrupted without errors.") - }) - - it("Fail", () => { - class Error1 { - readonly _tag = "WithTag" - } - expect(Cause.pretty(Cause.fail(new Error1()))).toEqual(`Error: {"_tag":"WithTag"}`) - class Error2 { - readonly _tag = "WithMessage" - readonly message = "my message" - } - expect(Cause.pretty(Cause.fail(new Error2()))).toEqual(`Error: {"_tag":"WithMessage","message":"my message"}`) - class Error3 { - readonly _tag = "WithName" - readonly name = "my name" - } - expect(Cause.pretty(Cause.fail(new Error3()))).toEqual(`Error: {"_tag":"WithName","name":"my name"}`) - class Error4 { - readonly _tag = "WithName" - readonly name = "my name" - readonly message = "my message" + describe("Cause prototype", () => { + describe("toJSON / [NodeInspectSymbol]", () => { + const expectJSON = (cause: Cause.Cause, expected: unknown) => { + assert.deepStrictEqual(cause.toJSON(), expected) + assert.deepStrictEqual(cause[NodeInspectSymbol](), expected) } - expect(Cause.pretty(Cause.fail(new Error4()))).toEqual( - `Error: {"_tag":"WithName","name":"my name","message":"my message"}` - ) - class Error5 { - readonly _tag = "WithToString" - toString() { - return "my string" - } - } - expect(Cause.pretty(Cause.fail(new Error5()))).toEqual(`Error: my string`) - }) - it("Interrupt", () => { - expect(Cause.pretty(Cause.interrupt(FiberId.none))).toEqual("All fibers interrupted without errors.") - expect(Cause.pretty(Cause.interrupt(FiberId.runtime(1, 0)))).toEqual( - "All fibers interrupted without errors." - ) - expect(Cause.pretty(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0))))).toEqual( - "All fibers interrupted without errors." - ) - }) - }) - - describe("toJSON", () => { - it("Empty", () => { - expect(Cause.empty.toJSON()).toEqual({ - _id: "Cause", - _tag: "Empty" + it("Empty", () => { + expectJSON(Cause.empty, { + _id: "Cause", + _tag: "Empty" + }) }) - }) - it("Fail", () => { - expect(Cause.fail(Option.some(1)).toJSON()).toEqual({ - _id: "Cause", - _tag: "Fail", - failure: { - _id: "Option", - _tag: "Some", - value: 1 - } + it("Fail", () => { + expectJSON(Cause.fail(Option.some(1)), { + _id: "Cause", + _tag: "Fail", + failure: { + _id: "Option", + _tag: "Some", + value: 1 + } + }) }) - }) - it("Die", () => { - expect(Cause.die(Option.some(1)).toJSON()).toEqual({ - _id: "Cause", - _tag: "Die", - defect: { - _id: "Option", - _tag: "Some", - value: 1 - } + it("Die", () => { + expectJSON(Cause.die(Option.some(1)), { + _id: "Cause", + _tag: "Die", + defect: { + _id: "Option", + _tag: "Some", + value: 1 + } + }) }) - }) - it("Interrupt", () => { - expect(Cause.interrupt(FiberId.none).toJSON()).toEqual({ - _id: "Cause", - _tag: "Interrupt", - fiberId: { - _id: "FiberId", - _tag: "None" - } - }) - expect(Cause.interrupt(FiberId.runtime(1, 0)).toJSON()).toEqual({ - _id: "Cause", - _tag: "Interrupt", - fiberId: { - _id: "FiberId", - _tag: "Runtime", - id: 1, - startTimeMillis: 0 - } - }) - expect(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0))).toJSON()).toEqual({ - _id: "Cause", - _tag: "Interrupt", - fiberId: { - _id: "FiberId", - _tag: "Composite", - left: { + it("Interrupt", () => { + expectJSON(Cause.interrupt(FiberId.none), { + _id: "Cause", + _tag: "Interrupt", + fiberId: { _id: "FiberId", _tag: "None" - }, - right: { + } + }) + expectJSON(Cause.interrupt(FiberId.runtime(1, 0)), { + _id: "Cause", + _tag: "Interrupt", + fiberId: { _id: "FiberId", _tag: "Runtime", id: 1, startTimeMillis: 0 } - } + }) + expectJSON(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0))), { + _id: "Cause", + _tag: "Interrupt", + fiberId: { + _id: "FiberId", + _tag: "Composite", + left: { + _id: "FiberId", + _tag: "None" + }, + right: { + _id: "FiberId", + _tag: "Runtime", + id: 1, + startTimeMillis: 0 + } + } + }) }) - }) - it("Sequential", () => { - expect(Cause.sequential(Cause.fail("failure 1"), Cause.fail("failure 2")).toJSON()).toStrictEqual({ - _id: "Cause", - _tag: "Sequential", - left: { + it("Sequential", () => { + expectJSON(Cause.sequential(Cause.fail("failure 1"), Cause.fail("failure 2")), { _id: "Cause", - _tag: "Fail", - failure: "failure 1" - }, - right: { + _tag: "Sequential", + left: { + _id: "Cause", + _tag: "Fail", + failure: "failure 1" + }, + right: { + _id: "Cause", + _tag: "Fail", + failure: "failure 2" + } + }) + }) + + it("Parallel", () => { + expectJSON(Cause.parallel(Cause.fail("failure 1"), Cause.fail("failure 2")), { _id: "Cause", - _tag: "Fail", - failure: "failure 2" - } + _tag: "Parallel", + left: { + _id: "Cause", + _tag: "Fail", + failure: "failure 1" + }, + right: { + _id: "Cause", + _tag: "Fail", + failure: "failure 2" + } + }) }) }) - it("Parallel", () => { - expect(Cause.parallel(Cause.fail("failure 1"), Cause.fail("failure 2")).toJSON()).toStrictEqual({ - _id: "Cause", - _tag: "Parallel", - left: { - _id: "Cause", - _tag: "Fail", - failure: "failure 1" - }, - right: { - _id: "Cause", - _tag: "Fail", - failure: "failure 2" - } + describe("toString", () => { + it("Empty", () => { + expect(String(Cause.empty)).toBe(`All fibers interrupted without errors.`) + }) + + it("Fail", () => { + expect(String(Cause.fail("my failure"))).toBe(`Error: my failure`) + expect(String(Cause.fail(new Error("my failure")))).includes(`Error: my failure`) + }) + + it("Die", () => { + expect(String(Cause.die("die message"))).toBe(`Error: die message`) + expect(String(Cause.die(new Error("die message")))).includes(`Error: die message`) + }) + + it("Interrupt", () => { + expect(String(Cause.interrupt(FiberId.none))).toBe(`All fibers interrupted without errors.`) + expect(String(Cause.interrupt(FiberId.runtime(1, 0)))).toBe(`All fibers interrupted without errors.`) + expect(String(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0))))).toBe( + `All fibers interrupted without errors.` + ) + }) + + it("Sequential", () => { + expect(String(Cause.sequential(Cause.fail("failure 1"), Cause.fail("failure 2")))).toBe( + `Error: failure 1\nError: failure 2` + ) + const actual = String(Cause.sequential(Cause.fail(new Error("failure 1")), Cause.fail(new Error("failure 2")))) + expect(actual).includes("Error: failure 1") + expect(actual).includes("Error: failure 2") + }) + + it("Parallel", () => { + expect(String(Cause.parallel(Cause.fail("failure 1"), Cause.fail("failure 2")))).toBe( + `Error: failure 1\nError: failure 2` + ) + const actual = String( + String(Cause.parallel(Cause.fail(new Error("failure 1")), Cause.fail(new Error("failure 2")))) + ) + expect(actual).includes("Error: failure 1") + expect(actual).includes("Error: failure 2") + }) + }) + + describe("Equal.symbol implementation", () => { + it("compares causes by value", () => { + assert.ok(Equal.equals(Cause.fail(0), Cause.fail(0))) + assert.ok(Equal.equals(Cause.die(0), Cause.die(0))) + assert.ok(!Equal.equals(Cause.fail(0), Cause.fail(1))) + assert.ok(!Equal.equals(Cause.die(0), Cause.die(1))) + }) + + it("is symmetric", () => { + fc.assert(fc.property(causes, causes, (causeA, causeB) => { + assert.strictEqual( + Equal.equals(causeA, causeB), + Equal.equals(causeB, causeA) + ) + })) + }) + + it("generates identical hashes for equal causes", () => { + fc.assert(fc.property(equalCauses, ([causeA, causeB]) => { + assert.strictEqual(Hash.hash(causeA), Hash.hash(causeB)) + })) + }) + + it("distinguishes different failure types", () => { + expect(Equal.equals(Cause.die(0), Cause.fail(0))).toBe(false) + expect( + Equal.equals( + Cause.parallel(Cause.fail("fail1"), Cause.die("fail2")), + Cause.parallel(Cause.fail("fail2"), Cause.die("fail1")) + ) + ).toBe(false) + expect( + Equal.equals( + Cause.sequential(Cause.fail("fail1"), Cause.die("fail2")), + Cause.parallel(Cause.fail("fail1"), Cause.die("fail2")) + ) + ).toBe(false) }) }) }) - describe("toString", () => { - it("Empty", () => { - expect(String(Cause.empty)).toEqual(`All fibers interrupted without errors.`) + describe("Guards", () => { + it("isCause", () => { + expect(Cause.isCause(empty)).toBe(true) + expect(Cause.isCause(failure)).toBe(true) + expect(Cause.isCause(defect)).toBe(true) + expect(Cause.isCause(interruption)).toBe(true) + expect(Cause.isCause(sequential)).toBe(true) + expect(Cause.isCause(parallel)).toBe(true) + + expect(Cause.isCause({})).toBe(false) }) - it("Fail", () => { - expect(String(Cause.fail("my failure"))).toEqual(`Error: my failure`) - expect(String(Cause.fail(new Error("my failure")))).includes(`Error: my failure`) + it("isEmptyType", () => { + expect(Cause.isEmptyType(empty)).toBe(true) + expect(Cause.isEmptyType(failure)).toBe(false) + expect(Cause.isEmptyType(defect)).toBe(false) + expect(Cause.isEmptyType(interruption)).toBe(false) + expect(Cause.isEmptyType(sequential)).toBe(false) + expect(Cause.isEmptyType(parallel)).toBe(false) }) - it("Die", () => { - expect(String(Cause.die("die message"))).toEqual(`Error: die message`) - expect(String(Cause.die(new Error("die message")))).includes(`Error: die message`) + it("isFailType", () => { + expect(Cause.isFailType(empty)).toBe(false) + expect(Cause.isFailType(failure)).toBe(true) + expect(Cause.isFailType(defect)).toBe(false) + expect(Cause.isFailType(interruption)).toBe(false) + expect(Cause.isFailType(sequential)).toBe(false) + expect(Cause.isFailType(parallel)).toBe(false) }) - it("Interrupt", () => { - expect(String(Cause.interrupt(FiberId.none))).toEqual(`All fibers interrupted without errors.`) - expect(String(Cause.interrupt(FiberId.runtime(1, 0)))).toEqual(`All fibers interrupted without errors.`) - expect(String(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0))))).toEqual( - `All fibers interrupted without errors.` - ) + it("isDieType", () => { + expect(Cause.isDieType(empty)).toBe(false) + expect(Cause.isDieType(failure)).toBe(false) + expect(Cause.isDieType(defect)).toBe(true) + expect(Cause.isDieType(interruption)).toBe(false) + expect(Cause.isDieType(sequential)).toBe(false) + expect(Cause.isDieType(parallel)).toBe(false) }) - it("Sequential", () => { - expect(String(Cause.sequential(Cause.fail("failure 1"), Cause.fail("failure 2")))).toEqual( - `Error: failure 1\nError: failure 2` - ) - const actual = String(Cause.sequential(Cause.fail(new Error("failure 1")), Cause.fail(new Error("failure 2")))) - expect(actual).includes("Error: failure 1") - expect(actual).includes("Error: failure 2") + it("isInterruptType", () => { + expect(Cause.isInterruptType(empty)).toBe(false) + expect(Cause.isInterruptType(failure)).toBe(false) + expect(Cause.isInterruptType(defect)).toBe(false) + expect(Cause.isInterruptType(interruption)).toBe(true) + expect(Cause.isInterruptType(sequential)).toBe(false) + expect(Cause.isInterruptType(parallel)).toBe(false) }) - it("Parallel", () => { - expect(String(Cause.parallel(Cause.fail("failure 1"), Cause.fail("failure 2")))).toEqual( - `Error: failure 1\nError: failure 2` - ) - const actual = String( - String(Cause.parallel(Cause.fail(new Error("failure 1")), Cause.fail(new Error("failure 2")))) - ) - expect(actual).includes("Error: failure 1") - expect(actual).includes("Error: failure 2") + it("isSequentialType", () => { + expect(Cause.isSequentialType(empty)).toBe(false) + expect(Cause.isSequentialType(failure)).toBe(false) + expect(Cause.isSequentialType(defect)).toBe(false) + expect(Cause.isSequentialType(interruption)).toBe(false) + expect(Cause.isSequentialType(sequential)).toBe(true) + expect(Cause.isSequentialType(parallel)).toBe(false) + }) + + it("isParallelType", () => { + expect(Cause.isParallelType(empty)).toBe(false) + expect(Cause.isParallelType(failure)).toBe(false) + expect(Cause.isParallelType(defect)).toBe(false) + expect(Cause.isParallelType(interruption)).toBe(false) + expect(Cause.isParallelType(sequential)).toBe(false) + expect(Cause.isParallelType(parallel)).toBe(true) }) }) - describe("Equal.symbol implementation", () => { - it("compares causes by value", () => { - assert.isTrue(Equal.equals(Cause.fail(0), Cause.fail(0))) - assert.isTrue(Equal.equals(Cause.die(0), Cause.die(0))) - assert.isFalse(Equal.equals(Cause.fail(0), Cause.fail(1))) - assert.isFalse(Equal.equals(Cause.die(0), Cause.die(1))) + describe("Getters", () => { + it("isEmpty", () => { + expect(Cause.isEmpty(empty)).toBe(true) + expect(Cause.isEmpty(Cause.sequential(empty, empty))).toBe(true) + expect(Cause.isEmpty(Cause.parallel(empty, empty))).toBe(true) + expect(Cause.isEmpty(Cause.parallel(empty, Cause.sequential(empty, empty)))).toBe(true) + expect(Cause.isEmpty(Cause.sequential(empty, Cause.parallel(empty, empty)))).toBe(true) + + expect(Cause.isEmpty(defect)).toBe(false) + expect(Cause.isEmpty(Cause.sequential(empty, failure))).toBe(false) + expect(Cause.isEmpty(Cause.parallel(empty, failure))).toBe(false) + expect(Cause.isEmpty(Cause.parallel(empty, Cause.sequential(empty, failure)))).toBe(false) + expect(Cause.isEmpty(Cause.sequential(empty, Cause.parallel(empty, failure)))).toBe(false) }) - it("is symmetric", () => { - fc.assert(fc.property(causes, causes, (causeA, causeB) => { - assert.strictEqual( - Equal.equals(causeA, causeB), - Equal.equals(causeB, causeA) - ) - })) + it("isFailure", () => { + expect(Cause.isFailure(failure)).toBe(true) + expect(Cause.isFailure(Cause.sequential(empty, failure))).toBe(true) + expect(Cause.isFailure(Cause.parallel(empty, failure))).toBe(true) + expect(Cause.isFailure(Cause.parallel(empty, Cause.sequential(empty, failure)))).toBe(true) + expect(Cause.isFailure(Cause.sequential(empty, Cause.parallel(empty, failure)))).toBe(true) + + expect(Cause.isFailure(Cause.sequential(empty, Cause.parallel(empty, empty)))).toBe(false) }) - it("generates identical hashes for equal causes", () => { - fc.assert(fc.property(equalCauses, ([causeA, causeB]) => { - assert.strictEqual(Hash.hash(causeA), Hash.hash(causeB)) - })) + it("isDie", () => { + expect(Cause.isDie(defect)).toBe(true) + expect(Cause.isDie(Cause.sequential(empty, defect))).toBe(true) + expect(Cause.isDie(Cause.parallel(empty, defect))).toBe(true) + expect(Cause.isDie(Cause.parallel(empty, Cause.sequential(empty, defect)))).toBe(true) + expect(Cause.isDie(Cause.sequential(empty, Cause.parallel(empty, defect)))).toBe(true) + + expect(Cause.isDie(Cause.sequential(empty, Cause.parallel(empty, empty)))).toBe(false) }) - it("distinguishes different failure types", () => { - expect(Equal.equals(Cause.die(0), Cause.fail(0))).toBe(false) - expect( - Equal.equals( - Cause.parallel(Cause.fail("fail1"), Cause.die("fail2")), - Cause.parallel(Cause.fail("fail2"), Cause.die("fail1")) - ) - ).toBe(false) + it("isInterrupted", () => { + expect(Cause.isInterrupted(interruption)).toBe(true) + expect(Cause.isInterrupted(Cause.sequential(empty, interruption))).toBe(true) + expect(Cause.isInterrupted(Cause.parallel(empty, interruption))).toBe(true) + expect(Cause.isInterrupted(Cause.parallel(empty, Cause.sequential(empty, interruption)))).toBe(true) + expect(Cause.isInterrupted(Cause.sequential(empty, Cause.parallel(empty, interruption)))).toBe(true) + + expect(Cause.isInterrupted(Cause.sequential(failure, interruption))).toBe(true) + expect(Cause.isInterrupted(Cause.parallel(failure, interruption))).toBe(true) + expect(Cause.isInterrupted(Cause.parallel(failure, Cause.sequential(empty, interruption)))).toBe(true) + expect(Cause.isInterrupted(Cause.sequential(failure, Cause.parallel(empty, interruption)))).toBe(true) + + expect(Cause.isInterrupted(Cause.sequential(empty, Cause.parallel(empty, empty)))).toBe(false) }) - }) - it("ensures isDie and keepDefects are consistent", () => { - fc.assert(fc.property(causes, (cause) => { - const result = Cause.keepDefects(cause) - if (Cause.isDie(cause)) { - assert.isTrue(Option.isSome(result)) - } else { - assert.isTrue(Option.isNone(result)) + it("isInterruptedOnly", () => { + expect(Cause.isInterruptedOnly(interruption)).toBe(true) + expect(Cause.isInterruptedOnly(Cause.sequential(empty, interruption))).toBe(true) + expect(Cause.isInterruptedOnly(Cause.parallel(empty, interruption))).toBe(true) + expect(Cause.isInterruptedOnly(Cause.parallel(empty, Cause.sequential(empty, interruption)))).toBe(true) + expect(Cause.isInterruptedOnly(Cause.sequential(empty, Cause.parallel(empty, interruption)))).toBe(true) + // Cause.empty is considered a valid candidate + expect(Cause.isInterruptedOnly(Cause.sequential(empty, Cause.parallel(empty, empty)))).toBe(true) + + expect(Cause.isInterruptedOnly(Cause.sequential(failure, interruption))).toBe(false) + expect(Cause.isInterruptedOnly(Cause.parallel(failure, interruption))).toBe(false) + expect(Cause.isInterruptedOnly(Cause.parallel(failure, Cause.sequential(empty, interruption)))).toBe(false) + expect(Cause.isInterruptedOnly(Cause.sequential(failure, Cause.parallel(empty, interruption)))).toBe(false) + }) + + describe("failures", () => { + it("should return a Chunk of all recoverable errors", () => { + const expectFailures = (cause: Cause.Cause, expected: Array) => { + assert.deepStrictEqual([...Cause.failures(cause)], expected) + } + expectFailures(empty, []) + expectFailures(failure, ["error"]) + expectFailures(Cause.parallel(Cause.fail("error1"), Cause.fail("error2")), ["error1", "error2"]) + expectFailures(Cause.sequential(Cause.fail("error1"), Cause.fail("error2")), ["error1", "error2"]) + expectFailures(Cause.parallel(failure, defect), ["error"]) + expectFailures(Cause.sequential(failure, defect), ["error"]) + expectFailures(Cause.sequential(interruption, Cause.parallel(empty, failure)), ["error"]) + }) + + it("fails safely for large parallel cause constructions", () => { + const n = 10_000 + const cause = Array.from({ length: n - 1 }, () => Cause.fail("fail")).reduce(Cause.parallel, Cause.fail("fail")) + const result = Cause.failures(cause) + assert.strictEqual(Array.from(result).length, n) + }) + }) + + it("defects", () => { + const expectDefects = (cause: Cause.Cause, expected: Array) => { + assert.deepStrictEqual([...Cause.defects(cause)], expected) } - })) - }) + expectDefects(empty, []) + expectDefects(defect, ["defect"]) + expectDefects(Cause.parallel(Cause.die("defect1"), Cause.die("defect2")), ["defect1", "defect2"]) + expectDefects(Cause.sequential(Cause.die("defect1"), Cause.die("defect2")), ["defect1", "defect2"]) + expectDefects(Cause.parallel(failure, defect), ["defect"]) + expectDefects(Cause.sequential(failure, defect), ["defect"]) + expectDefects(Cause.sequential(interruption, Cause.parallel(empty, defect)), ["defect"]) + }) - it("fails safely for large parallel cause constructions", () => { - const n = 10_000 - const cause = Array.from({ length: n - 1 }, () => Cause.fail("fail")).reduce(Cause.parallel, Cause.fail("fail")) - const result = Cause.failures(cause) - assert.strictEqual(Array.from(result).length, n) - }) + it("interruptors", () => { + const expectInterruptors = (cause: Cause.Cause, expected: Array) => { + assert.deepStrictEqual([...Cause.interruptors(cause)], expected) + } + expectInterruptors(empty, []) + expectInterruptors(interruption, [FiberId.runtime(1, 0)]) + expectInterruptors( + Cause.sequential( + Cause.interrupt(FiberId.runtime(1, 0)), + Cause.parallel(empty, Cause.interrupt(FiberId.runtime(2, 0))) + ), + [FiberId.runtime(2, 0), FiberId.runtime(1, 0)] + ) + }) - describe("flatMap", () => { - it("obeys left identity", () => { - fc.assert(fc.property(causes, (cause) => { - const left = cause.pipe(Cause.flatMap(Cause.fail)) - const right = cause - assert.isTrue(Equal.equals(left, right)) - })) + it("size", () => { + expect(Cause.size(empty)).toBe(0) + expect(Cause.size(failure)).toBe(1) + expect(Cause.size(defect)).toBe(1) + expect(Cause.size(Cause.parallel(Cause.fail("error1"), Cause.fail("error2")))).toBe(2) + expect(Cause.size(Cause.sequential(Cause.fail("error1"), Cause.fail("error2")))).toBe(2) + expect(Cause.size(Cause.parallel(failure, defect))).toBe(2) + expect(Cause.size(Cause.sequential(failure, defect))).toBe(2) + expect(Cause.size(Cause.sequential(interruption, Cause.parallel(empty, failure)))).toBe(2) + expect(Cause.size(Cause.sequential(interruption, Cause.parallel(defect, failure)))).toBe(3) }) - it("obeys right identity", () => { - fc.assert(fc.property(errors, errorCauseFunctions, (error, f) => { - const left = Cause.fail(error).pipe(Cause.flatMap(f)) - const right = f(error) - assert.isTrue(Equal.equals(left, right)) - })) + it("failureOption", () => { + const expectFailureOption = (cause: Cause.Cause, expected: Option.Option) => { + assert.deepStrictEqual(Cause.failureOption(cause), expected) + } + expectFailureOption(empty, Option.none()) + expectFailureOption(failure, Option.some("error")) + expectFailureOption(Cause.sequential(Cause.fail("error1"), Cause.fail("error2")), Option.some("error1")) + expectFailureOption(Cause.parallel(Cause.fail("error1"), Cause.fail("error2")), Option.some("error1")) + expectFailureOption(Cause.parallel(failure, defect), Option.some("error")) + expectFailureOption(Cause.sequential(failure, defect), Option.some("error")) + expectFailureOption(Cause.sequential(interruption, Cause.parallel(empty, failure)), Option.some("error")) + }) + + it("failureOrCause", () => { + const expectLeft = (cause: Cause.Cause, expected: E) => { + assert.deepStrictEqual(Cause.failureOrCause(cause), Either.left(expected)) + } + const expectRight = (cause: Cause.Cause) => { + assert.deepStrictEqual(Cause.failureOrCause(cause), Either.right(cause)) + } + + expectLeft(failure, "error") + expectLeft(Cause.parallel(Cause.fail("error1"), Cause.fail("error2")), "error1") + expectLeft(Cause.sequential(Cause.fail("error1"), Cause.fail("error2")), "error1") + expectLeft(Cause.sequential(interruption, Cause.parallel(empty, failure)), "error") + + expectRight(empty) + expectRight(defect) + expectRight(interruption) + expectRight(Cause.sequential(interruption, Cause.parallel(empty, defect))) + }) + + it("flipCauseOption", () => { + assert.deepStrictEqual(Cause.flipCauseOption(empty), Option.some(empty)) + assert.deepStrictEqual(Cause.flipCauseOption(defect), Option.some(defect)) + assert.deepStrictEqual(Cause.flipCauseOption(interruption), Option.some(interruption)) + assert.deepStrictEqual(Cause.flipCauseOption(Cause.fail(Option.none())), Option.none()) + assert.deepStrictEqual(Cause.flipCauseOption(Cause.fail(Option.some("error"))), Option.some(Cause.fail("error"))) + // sequential + assert.deepStrictEqual( + Cause.flipCauseOption(Cause.sequential(Cause.fail(Option.some("error1")), Cause.fail(Option.some("error2")))), + Option.some(Cause.sequential(Cause.fail("error1"), Cause.fail("error2"))) + ) + assert.deepStrictEqual( + Cause.flipCauseOption(Cause.sequential(Cause.fail(Option.some("error1")), Cause.fail(Option.none()))), + Option.some(Cause.fail("error1")) + ) + assert.deepStrictEqual( + Cause.flipCauseOption(Cause.sequential(Cause.fail(Option.none()), Cause.fail(Option.some("error2")))), + Option.some(Cause.fail("error2")) + ) + assert.deepStrictEqual( + Cause.flipCauseOption(Cause.sequential(Cause.fail(Option.none()), Cause.fail(Option.none()))), + Option.none() + ) + // parallel + assert.deepStrictEqual( + Cause.flipCauseOption(Cause.parallel(Cause.fail(Option.some("error1")), Cause.fail(Option.some("error2")))), + Option.some(Cause.parallel(Cause.fail("error1"), Cause.fail("error2"))) + ) + assert.deepStrictEqual( + Cause.flipCauseOption(Cause.parallel(Cause.fail(Option.some("error1")), Cause.fail(Option.none()))), + Option.some(Cause.fail("error1")) + ) + assert.deepStrictEqual( + Cause.flipCauseOption(Cause.parallel(Cause.fail(Option.none()), Cause.fail(Option.some("error2")))), + Option.some(Cause.fail("error2")) + ) + assert.deepStrictEqual( + Cause.flipCauseOption(Cause.parallel(Cause.fail(Option.none()), Cause.fail(Option.none()))), + Option.none() + ) + }) + + it("dieOption", () => { + const expectDieOption = (cause: Cause.Cause, expected: Option.Option) => { + assert.deepStrictEqual(Cause.dieOption(cause), expected) + } + expectDieOption(empty, Option.none()) + expectDieOption(defect, Option.some("defect")) + expectDieOption(Cause.parallel(Cause.die("defect1"), Cause.die("defect2")), Option.some("defect1")) + expectDieOption(Cause.sequential(Cause.die("defect1"), Cause.die("defect2")), Option.some("defect1")) + expectDieOption(Cause.parallel(failure, defect), Option.some("defect")) + expectDieOption(Cause.sequential(failure, defect), Option.some("defect")) + expectDieOption(Cause.sequential(interruption, Cause.parallel(empty, defect)), Option.some("defect")) + }) + + it("interruptOption", () => { + const expectInterruptOption = (cause: Cause.Cause, expected: Option.Option) => { + assert.deepStrictEqual(Cause.interruptOption(cause), expected) + } + expectInterruptOption(empty, Option.none()) + expectInterruptOption(interruption, Option.some(FiberId.runtime(1, 0))) + expectInterruptOption( + Cause.sequential( + Cause.interrupt(FiberId.runtime(1, 0)), + Cause.parallel(empty, Cause.interrupt(FiberId.runtime(2, 0))) + ), + Option.some(FiberId.runtime(1, 0)) + ) + }) + + it("keepDefects", () => { + assert.deepStrictEqual(Cause.keepDefects(empty), Option.none()) + assert.deepStrictEqual(Cause.keepDefects(failure), Option.none()) + assert.deepStrictEqual(Cause.keepDefects(defect), Option.some(defect)) + assert.deepStrictEqual( + Cause.keepDefects(Cause.sequential(Cause.die("defect1"), Cause.die("defect2"))), + Option.some(Cause.sequential(Cause.die("defect1"), Cause.die("defect2"))) + ) + assert.deepStrictEqual(Cause.keepDefects(Cause.sequential(empty, empty)), Option.none()) + assert.deepStrictEqual(Cause.keepDefects(Cause.sequential(defect, failure)), Option.some(defect)) + assert.deepStrictEqual(Cause.keepDefects(Cause.parallel(empty, empty)), Option.none()) + assert.deepStrictEqual(Cause.keepDefects(Cause.parallel(defect, failure)), Option.some(defect)) + assert.deepStrictEqual( + Cause.keepDefects(Cause.parallel(Cause.die("defect1"), Cause.die("defect2"))), + Option.some(Cause.parallel(Cause.die("defect1"), Cause.die("defect2"))) + ) + assert.deepStrictEqual( + Cause.keepDefects( + Cause.sequential(failure, Cause.parallel(Cause.die("defect1"), Cause.die("defect2"))) + ), + Option.some(Cause.parallel(Cause.die("defect1"), Cause.die("defect2"))) + ) + assert.deepStrictEqual( + Cause.keepDefects( + Cause.sequential(Cause.die("defect1"), Cause.parallel(failure, Cause.die("defect2"))) + ), + Option.some(Cause.sequential(Cause.die("defect1"), Cause.die("defect2"))) + ) }) - it("is associative", () => { - fc.assert(fc.property(causes, errorCauseFunctions, errorCauseFunctions, (cause, f, g) => { - const left = cause.pipe(Cause.flatMap(f), Cause.flatMap(g)) - const right = cause.pipe(Cause.flatMap((error) => f(error).pipe(Cause.flatMap(g)))) - assert.isTrue(Equal.equals(left, right)) + it("ensures isDie and keepDefects are consistent", () => { + fc.assert(fc.property(causes, (cause) => { + const result = Cause.keepDefects(cause) + if (Cause.isDie(cause)) { + return Option.isSome(result) + } else { + return Option.isNone(result) + } })) }) - }) - it("andThen returns the second cause if the first one is failing", () => { - const err1 = Cause.fail("err1") - const err2 = Cause.fail("err2") - expect(err1.pipe(Cause.andThen(() => err2))).toStrictEqual(err2) - expect(err1.pipe(Cause.andThen(err2))).toStrictEqual(err2) - expect(Cause.andThen(err1, () => err2)).toStrictEqual(err2) - expect(Cause.andThen(err1, err2)).toStrictEqual(err2) - }) + // TODO: what's the point of this API? + it("linearize", () => { + const expectLinearize = (cause: Cause.Cause, expected: Array>) => { + assert.deepStrictEqual([...Cause.linearize(cause)], expected) + } + expectLinearize(empty, []) + expectLinearize(failure, [failure]) + expectLinearize(defect, [defect]) + expectLinearize(interruption, [interruption]) + expectLinearize(Cause.sequential(failure, defect), [Cause.sequential(failure, defect)]) + expectLinearize(Cause.parallel(failure, defect), [Cause.parallel(failure, defect)]) + expectLinearize(Cause.sequential(failure, Cause.sequential(interruption, defect)), [ + Cause.sequential(failure, Cause.sequential(interruption, defect)) + ]) + expectLinearize(Cause.parallel(failure, Cause.parallel(interruption, defect)), [ + Cause.parallel(failure, Cause.parallel(interruption, defect)) + ]) + expectLinearize( + Cause.sequential( + Cause.sequential(Cause.fail("error1"), Cause.fail("error2")), + Cause.sequential(Cause.fail("error3"), Cause.fail("error4")) + ), + [ + Cause.sequential( + Cause.sequential(Cause.fail("error1"), Cause.fail("error2")), + Cause.sequential(Cause.fail("error3"), Cause.fail("error4")) + ) + ] + ) + expectLinearize( + Cause.parallel( + Cause.parallel(Cause.fail("error1"), Cause.fail("error2")), + Cause.parallel(Cause.fail("error3"), Cause.fail("error4")) + ), + [ + Cause.parallel( + Cause.parallel(Cause.fail("error1"), Cause.fail("error2")), + Cause.parallel(Cause.fail("error3"), Cause.fail("error4")) + ) + ] + ) + }) + + it("stripFailures", () => { + const expectStripFailures = (cause: Cause.Cause, expected: Cause.Cause) => { + assert.deepStrictEqual(Cause.stripFailures(cause), expected) + } + expectStripFailures(empty, empty) + expectStripFailures(failure, empty) + expectStripFailures(defect, defect) + expectStripFailures(interruption, interruption) + expectStripFailures(interruption, interruption) + expectStripFailures(Cause.sequential(failure, defect), Cause.sequential(empty, defect)) + expectStripFailures(Cause.parallel(failure, defect), Cause.parallel(empty, defect)) + }) - describe("stripSomeDefects", () => { - it("removes matching defects and returns the remainder if any exist", () => { + it("stripSomeDefects", () => { const cause1 = Cause.die({ _tag: "NumberFormatException", msg: "can't parse to int" @@ -360,55 +692,382 @@ describe("Cause", () => { _tag: "ArithmeticException", msg: "division by zero" }) - const cause = Cause.parallel(cause1, cause2) - const stripped = cause.pipe( - Cause.stripSomeDefects((defect) => - Predicate.isTagged(defect, "NumberFormatException") - ? Option.some(defect) : - Option.none() - ) + const stripNumberFormatException = Cause.stripSomeDefects((defect) => + Predicate.isTagged(defect, "NumberFormatException") + ? Option.some(defect) : + Option.none() + ) + assert.deepStrictEqual(stripNumberFormatException(empty), Option.some(empty)) + assert.deepStrictEqual(stripNumberFormatException(failure), Option.some(failure)) + assert.deepStrictEqual(stripNumberFormatException(interruption), Option.some(interruption)) + assert.deepStrictEqual(stripNumberFormatException(cause1), Option.none()) + assert.deepStrictEqual(stripNumberFormatException(Cause.sequential(cause1, cause1)), Option.none()) + assert.deepStrictEqual(stripNumberFormatException(Cause.sequential(cause1, cause2)), Option.some(cause2)) + assert.deepStrictEqual(stripNumberFormatException(Cause.sequential(cause2, cause1)), Option.some(cause2)) + assert.deepStrictEqual( + stripNumberFormatException(Cause.sequential(cause2, cause2)), + Option.some(Cause.sequential(cause2, cause2)) ) - assert.isTrue(Equal.equals(stripped, Option.some(cause2))) + assert.deepStrictEqual(stripNumberFormatException(Cause.parallel(cause1, cause1)), Option.none()) + assert.deepStrictEqual(stripNumberFormatException(Cause.parallel(cause1, cause2)), Option.some(cause2)) + assert.deepStrictEqual(stripNumberFormatException(Cause.parallel(cause2, cause1)), Option.some(cause2)) + assert.deepStrictEqual( + stripNumberFormatException(Cause.parallel(cause2, cause2)), + Option.some(Cause.parallel(cause2, cause2)) + ) + }) + }) + + describe("Mapping", () => { + it("as", () => { + const expectAs = (cause: Cause.Cause, expected: Cause.Cause) => { + assert.deepStrictEqual(Cause.as(cause, 2), expected) + } + expectAs(empty, empty) + expectAs(failure, Cause.fail(2)) + expectAs(defect, defect) + expectAs(interruption, interruption) + expectAs(sequential, Cause.sequential(Cause.fail(2), defect)) + expectAs(parallel, Cause.parallel(Cause.fail(2), defect)) }) - it("returns None if all defects match and are thus removed", () => { - const cause = Cause.die({ _tag: "NumberFormatException", msg: "can't parse to int" }) - const stripped = cause.pipe( - Cause.stripSomeDefects((defect) => - Predicate.isTagged(defect, "NumberFormatException") - ? Option.some(defect) : - Option.none() + it("map", () => { + const expectMap = (cause: Cause.Cause, expected: Cause.Cause) => { + assert.deepStrictEqual(Cause.map(cause, () => 2), expected) + } + expectMap(empty, empty) + expectMap(failure, Cause.fail(2)) + expectMap(defect, defect) + expectMap(interruption, interruption) + expectMap(sequential, Cause.sequential(Cause.fail(2), defect)) + expectMap(parallel, Cause.parallel(Cause.fail(2), defect)) + }) + }) + + describe("Sequencing", () => { + describe("flatMap", () => { + it("obeys left identity", () => { + fc.assert(fc.property(causes, (cause) => { + const left = cause.pipe(Cause.flatMap(Cause.fail)) + const right = cause + assert.ok(Equal.equals(left, right)) + })) + }) + + it("obeys right identity", () => { + fc.assert(fc.property(errors, errorCauseFunctions, (error, f) => { + const left = Cause.fail(error).pipe(Cause.flatMap(f)) + const right = f(error) + assert.ok(Equal.equals(left, right)) + })) + }) + + it("is associative", () => { + fc.assert(fc.property(causes, errorCauseFunctions, errorCauseFunctions, (cause, f, g) => { + const left = cause.pipe(Cause.flatMap(f), Cause.flatMap(g)) + const right = cause.pipe(Cause.flatMap((error) => f(error).pipe(Cause.flatMap(g)))) + assert.ok(Equal.equals(left, right)) + })) + }) + }) + + it("andThen returns the second cause if the first one is failing", () => { + const err1 = Cause.fail("err1") + const err2 = Cause.fail("err2") + assert.deepStrictEqual(err1.pipe(Cause.andThen(() => err2)), err2) + assert.deepStrictEqual(err1.pipe(Cause.andThen(err2)), err2) + assert.deepStrictEqual(Cause.andThen(err1, () => err2), err2) + assert.deepStrictEqual(Cause.andThen(err1, err2), err2) + }) + + it("flatten", () => { + const expectFlatten = (cause: Cause.Cause>, expected: Cause.Cause) => { + assert.deepStrictEqual(Cause.flatten(cause), expected) + } + expectFlatten(Cause.fail(empty), empty) + expectFlatten(Cause.fail(failure), failure) + expectFlatten(Cause.fail(defect), defect) + expectFlatten(Cause.fail(interruption), interruption) + expectFlatten(Cause.fail(sequential), sequential) + expectFlatten(Cause.fail(parallel), parallel) + }) + }) + + describe("Elements", () => { + it("contains", () => { + const expectContains = (cause: Cause.Cause, expected: Cause.Cause) => { + assert.ok(Cause.contains(cause, expected)) + } + + expectContains(empty, empty) + expectContains(failure, failure) + expectContains(defect, defect) + expectContains(interruption, interruption) + expectContains(sequential, sequential) + expectContains(parallel, parallel) + expectContains(sequential, failure) + expectContains(sequential, defect) + expectContains(parallel, failure) + expectContains(parallel, defect) + }) + + it("find", () => { + const expectFind = (cause: Cause.Cause, expected: Option.Option) => { + assert.deepStrictEqual( + Cause.find( + cause, + (cause) => + Cause.isFailType(cause) && Predicate.isString(cause.error) ? Option.some(cause.error) : Option.none() + ), + expected ) - ) - assert.isTrue(Equal.equals(stripped, Option.none())) + } + + expectFind(empty, Option.none()) + expectFind(failure, Option.some("error")) + expectFind(defect, Option.none()) + expectFind(interruption, Option.none()) + expectFind(sequential, Option.some("error")) + expectFind(parallel, Option.some("error")) }) }) - describe("UnknownException", () => { - it("exposes its `error` property", () => { - const err1 = new Cause.UnknownException("my message") - expect(err1.error).toEqual("my message") - const err2 = new Cause.UnknownException(new Error("my error")) - expect(err2.error).toBeInstanceOf(Error) - expect((err2.error as Error).message).toEqual("my error") + describe("Destructors", () => { + it("squash", () => { + const expectSquash = (cause: Cause.Cause, expected: unknown) => { + assert.deepStrictEqual(Cause.squash(cause), expected) + } + + expectSquash(empty, new Cause.InterruptedException("Interrupted by fibers: ")) + expectSquash(failure, "error") + expectSquash(defect, "defect") + expectSquash(interruption, new Cause.InterruptedException("Interrupted by fibers: #1")) + expectSquash(sequential, "error") + expectSquash(parallel, "error") + expectSquash(Cause.sequential(empty, defect), "defect") + expectSquash(Cause.parallel(empty, defect), "defect") }) - it("exposes its `cause` property", () => { - const err1 = new Cause.UnknownException("my message") - expect(err1.cause).toEqual("my message") - const err2 = new Cause.UnknownException(new Error("my error")) - expect(err2.cause).toBeInstanceOf(Error) - expect((err2.cause as Error).message).toEqual("my error") + it.todo("squashWith", () => { }) + }) - it("uses a default message when none is provided", () => { - const err1 = new Cause.UnknownException("my message") - expect(err1.message).toEqual("An unknown error occurred") + describe("Filtering", () => { + it("filter", () => { + const expectFilter = (cause: Cause.Cause, expected: Cause.Cause) => { + assert.deepStrictEqual( + Cause.filter( + cause, + (cause) => Cause.isFailType(cause) && Predicate.isString(cause.error) && cause.error === "error" + ), + expected + ) + } + + expectFilter(empty, empty) + expectFilter(failure, failure) + expectFilter(defect, defect) + expectFilter(interruption, interruption) + expectFilter(sequential, failure) + expectFilter(Cause.sequential(failure, failure), Cause.sequential(failure, failure)) + expectFilter(Cause.sequential(defect, failure), failure) + expectFilter(Cause.sequential(defect, defect), empty) + expectFilter(parallel, failure) + expectFilter(Cause.parallel(failure, failure), Cause.parallel(failure, failure)) + expectFilter(Cause.parallel(defect, failure), failure) + expectFilter(Cause.parallel(defect, defect), empty) }) + }) - it("accepts a custom override message", () => { - const err1 = new Cause.UnknownException(new Error("my error"), "my message") - expect(err1.message).toEqual("my message") + describe("Matching", () => { + it("match", () => { + const expectMatch = (cause: Cause.Cause, expected: string) => { + assert.strictEqual( + Cause.match(cause, { + onEmpty: "Empty", + onFail: () => "Fail", + onDie: () => "Die", + onInterrupt: () => "Interrupt", + onSequential: () => "Sequential", + onParallel: () => "Parallel" + }), + expected + ) + } + expectMatch(empty, "Empty") + expectMatch(failure, "Fail") + expectMatch(defect, "Die") + expectMatch(interruption, "Interrupt") + expectMatch(sequential, "Sequential") + expectMatch(parallel, "Parallel") + }) + }) + + describe("Reducing", () => { + it.todo("reduce", () => { + }) + + it.todo("reduceWithContext", () => { + }) + }) + + describe("Formatting", () => { + it("prettyErrors", () => { + assert.deepStrictEqual(Cause.prettyErrors(empty), []) + assert.deepStrictEqual(Cause.prettyErrors(failure), [new internal.PrettyError("error")]) + assert.deepStrictEqual(Cause.prettyErrors(defect), [new internal.PrettyError("defect")]) + assert.deepStrictEqual(Cause.prettyErrors(interruption), []) + assert.deepStrictEqual(Cause.prettyErrors(sequential), [ + new internal.PrettyError("error"), + new internal.PrettyError("defect") + ]) + assert.deepStrictEqual(Cause.prettyErrors(parallel), [ + new internal.PrettyError("error"), + new internal.PrettyError("defect") + ]) + }) + + describe("pretty", () => { + const simplifyStackTrace = (s: string): Array => { + return Arr.filterMap(s.split("\n"), (s) => { + const t = s.trimStart() + if (t === "}") { + return Option.none() + } + if (t.startsWith("at [")) { + return Option.some(t.substring(0, t.indexOf("] ") + 1)) + } + if (t.startsWith("at ")) { + return Option.none() + } + return Option.some(t) + }) + } + + describe("renderErrorCause: false", () => { + const expectPretty = (cause: Cause.Cause, expected: string | undefined) => { + assert.deepStrictEqual(Cause.pretty(cause), expected) + assert.deepStrictEqual(Cause.pretty(cause, { renderErrorCause: false }), expected) + } + + it("handles array-based errors without throwing", () => { + expectPretty(Cause.fail([{ toString: "" }]), `Error: [{"toString":""}]`) + }) + + it("Empty", () => { + expectPretty(empty, "All fibers interrupted without errors.") + }) + + it("Fail", () => { + class Error1 { + readonly _tag = "WithTag" + } + expectPretty(Cause.fail(new Error1()), `Error: {"_tag":"WithTag"}`) + class Error2 { + readonly _tag = "WithMessage" + readonly message = "my message" + } + expectPretty(Cause.fail(new Error2()), `Error: {"_tag":"WithMessage","message":"my message"}`) + class Error3 { + readonly _tag = "WithName" + readonly name = "my name" + } + expectPretty(Cause.fail(new Error3()), `Error: {"_tag":"WithName","name":"my name"}`) + class Error4 { + readonly _tag = "WithName" + readonly name = "my name" + readonly message = "my message" + } + expectPretty(Cause.fail(new Error4()), `Error: {"_tag":"WithName","name":"my name","message":"my message"}`) + class Error5 { + readonly _tag = "WithToString" + toString() { + return "my string" + } + } + expectPretty(Cause.fail(new Error5()), `Error: my string`) + + const err1 = new Error("message", { cause: "my cause" }) + expectPretty(Cause.fail(err1), err1.stack) + }) + + it("Interrupt", () => { + expect(Cause.pretty(Cause.interrupt(FiberId.none))).toBe("All fibers interrupted without errors.") + expect(Cause.pretty(Cause.interrupt(FiberId.runtime(1, 0)))).toBe( + "All fibers interrupted without errors." + ) + expect(Cause.pretty(Cause.interrupt(FiberId.composite(FiberId.none, FiberId.runtime(1, 0))))).toBe( + "All fibers interrupted without errors." + ) + }) + + describe("Die", () => { + it("with span", () => { + const exit: any = Effect.die(new Error("my message")).pipe( + Effect.withSpan("[myspan]"), + Effect.exit, + Effect.runSync + ) + const cause = exit.cause + const pretty = Cause.pretty(cause) + assert.deepStrictEqual(simplifyStackTrace(pretty), [`Error: my message`, "at [myspan]"]) + }) + }) + }) + + describe("renderErrorCause: true", () => { + describe("Fail", () => { + it("no cause", () => { + const pretty = Cause.pretty(Cause.fail(new Error("my message")), { renderErrorCause: true }) + assert.deepStrictEqual(simplifyStackTrace(pretty), ["Error: my message"]) + }) + + it("string cause", () => { + const pretty = Cause.pretty(Cause.fail(new Error("my message", { cause: "my cause" })), { + renderErrorCause: true + }) + assert.deepStrictEqual(simplifyStackTrace(pretty), ["Error: my message", "[cause]: Error: my cause"]) + }) + + it("error cause", () => { + const pretty = Cause.pretty(Cause.fail(new Error("my message", { cause: new Error("my cause") })), { + renderErrorCause: true + }) + assert.deepStrictEqual(simplifyStackTrace(pretty), ["Error: my message", "[cause]: Error: my cause"]) + }) + + it("error cause with nested cause", () => { + const pretty = Cause.pretty( + Cause.fail(new Error("my message", { cause: new Error("my cause", { cause: "nested cause" }) })), + { + renderErrorCause: true + } + ) + assert.deepStrictEqual(simplifyStackTrace(pretty), [ + "Error: my message", + "[cause]: Error: my cause", + "[cause]: Error: nested cause" + ]) + }) + }) + + describe("Die", () => { + it("with span", () => { + const exit: any = Effect.die(new Error("my message", { cause: "my cause" })).pipe( + Effect.withSpan("[myspan]"), + Effect.exit, + Effect.runSync + ) + const cause = exit.cause + const pretty = Cause.pretty(cause, { renderErrorCause: true }) + assert.deepStrictEqual(simplifyStackTrace(pretty), [ + `Error: my message`, + "at [myspan]", + "[cause]: Error: my cause" + ]) + }) + }) + }) }) }) }) diff --git a/packages/effect/vitest.config.ts b/packages/effect/vitest.config.ts index 0389a805a67..c97c0d37009 100644 --- a/packages/effect/vitest.config.ts +++ b/packages/effect/vitest.config.ts @@ -5,7 +5,7 @@ const config: UserConfigExport = { // test: { // coverage: { // reporter: ["html"], - // include: ["src/Duration.ts"] + // include: ["src/Cause.ts", "src/internal/cause.ts"] // } // } }