Expected errors are tracked at the type level by the Effect data type in the “Error channel”:
This means that the Effect type captures not only what the program returns on success but also what type of error it might produce.
Example (Creating an Effect That Can Fail)
In this example, we define a program that might randomly fail with an HttpError.
The type of program tells us that it can either return a string or fail with an HttpError:
In this case, we use a class to represent the HttpError type, which allows us to define both the error type and a constructor. However, you can use whatever
you like to model your error types.
It’s also worth noting that we added a readonly _tag field to the class:
This discriminant field will be useful when we discuss APIs like Effect.catchTag, which help in handling specific error types.
Error Tracking
In Effect, if a program can fail with multiple types of errors, they are automatically tracked as a union of those error types.
This allows you to know exactly what errors can occur during execution, making error handling more precise and predictable.
The example below illustrates how errors are automatically tracked for you.
Example (Automatically Tracking Errors)
Effect automatically keeps track of the possible errors that can occur during the execution of the program as a union:
indicating that it can potentially fail with either a HttpError or a ValidationError.
Short-Circuiting
When working with APIs like Effect.gen, Effect.map, Effect.flatMap, and Effect.andThen, it’s important to understand how they handle errors.
These APIs are designed to short-circuit the execution upon encountering the first error.
What does this mean for you as a developer? Well, let’s say you have a chain of operations or a collection of effects to be executed in sequence. If any error occurs during the execution of one of these effects, the remaining computations will be skipped, and the error will be propagated to the final result.
In simpler terms, the short-circuiting behavior ensures that if something goes wrong at any step of your program, it won’t waste time executing unnecessary computations. Instead, it will immediately stop and return the error to let you know that something went wrong.
Example (Short-Circuiting Behavior)
This code snippet demonstrates the short-circuiting behavior when an error occurs.
Each operation depends on the successful execution of the previous one.
If any error occurs, the execution is short-circuited, and the error is propagated.
In this specific example, task3 is never executed because an error occurs in task2.
Catching All Errors
either
The Effect.either function transforms an Effect<A, E, R> into an effect that encapsulates both potential failure and success within an Either data type:
This means if you have an effect with the following type:
and you call Effect.either on it, the type becomes:
The resulting effect cannot fail because the potential failure is now represented within the Either’s Left type.
The error type of the returned Effect is specified as never, confirming that the effect is structured to not fail.
By yielding an Either, we gain the ability to “pattern match” on this type to handle both failure and success cases within the generator function.
Example (Using Effect.either to Handle Errors)
As you can see since all errors are handled, the error type of the resulting effect recovered is never:
We can make the code less verbose by using the Either.match function, which directly accepts the two callback functions for handling errors and successful values:
Example (Simplifying with Either.match)
catchAll
The Effect.catchAll function allows you to catch any error that occurs in the program and provide a fallback.
Example (Catching All Errors with Effect.catchAll)
We can observe that the type in the error channel of our program has changed to never:
indicating that all errors have been handled.
Catching Some Errors
either
The Effect.either function transforms an Effect<A, E, R> into an effect that encapsulates both potential failure and success within an Either data type:
This means if you have an effect with the following type:
and you call Effect.either on it, the type becomes:
The resulting effect cannot fail because the potential failure is now represented within the Either’s Left type.
The error type of the returned Effect is specified as never, confirming that the effect is structured to not fail.
By yielding an Either, we gain the ability to “pattern match” on this type to handle both failure and success cases within the generator function.
Example (Handling Specific Errors with Effect.either)
We can observe that the type in the error channel of our program has changed to only show ValidationError:
indicating that HttpError has been handled.
If we also want to handle ValidationError, we can easily add another case to our code:
We can observe that the type in the error channel has changed to never:
indicating that all errors have been handled.
catchSome
If we want to catch and recover from only some types of errors and effectfully attempt recovery, we can use the Effect.catchSome function.
Example (Handling Specific Errors with Effect.catchSome)
In the code above, Effect.catchSome takes a function that examines the error and decides whether to attempt recovery or not. If the error matches a specific condition, recovery can be attempted by returning Option.some(effect). If no recovery is possible, you can simply return Option.none().
It’s important to note that while Effect.catchSome lets you catch specific errors, it doesn’t alter the error type itself.
Therefore, the resulting effect will still have the same error type as the original effect:
catchIf
Similar to Effect.catchSome, the function Effect.catchIf allows you to recover from specific errors based on a predicate.
Example (Catching Specific Errors with a Predicate)
It’s important to note that for TypeScript versions < 5.5, while Effect.catchIf lets you catch specific errors, it doesn’t alter the error type itself.
Therefore, the resulting effect will still have the same error type as the original effect:
In TypeScript versions >= 5.5, improved type narrowing causes the resulting error type to be inferred as ValidationError.
Workaround For TypeScript versions < 5.5
If you provide a user-defined type guard instead of a predicate, the resulting error type will be pruned, returning an Effect<string, ValidationError, never>:
catchTag
If your program’s errors are all tagged with a _tag field that acts as a discriminator you can use the Effect.catchTag function to catch and handle specific errors with precision.
Example (Handling Errors by Tag with Effect.catchTag)
In the example above, the Effect.catchTag function allows us to handle HttpError specifically.
If a HttpError occurs during the execution of the program, the provided error handler function will be invoked,
and the program will proceed with the recovery logic specified within the handler.
We can observe that the type in the error channel of our program has changed to only show ValidationError:
indicating that HttpError has been handled.
If we also wanted to handle ValidationError, we can simply add another catchTag:
Example (Handling Multiple Error Types with catchTag)
We can observe that the type in the error channel of our program has changed to never:
indicating that all errors have been handled.
catchTags
Instead of using the Effect.catchTag function multiple times to handle individual error types, we have a more convenient option called Effect.catchTags. With Effect.catchTags, we can handle multiple errors in a single block of code.
Example (Handling Multiple Error Types with catchTags)
This function takes an object where each property represents a specific error _tag ("HttpError" and "ValidationError" in this case),
and the corresponding value is the error handler function to be executed when that particular error occurs.