Expected Errors
On this page
In this guide you will learn:
- How Effect represents expected errors
- The tools Effect provides for robust and comprehensive error management
As we saw in the guide Creating Effects, we can use the fail
constructor to create an Effect
that represents an error:
ts
import {Effect } from "effect"classHttpError {readonly_tag = "HttpError"}constprogram =Effect .fail (newHttpError ())
ts
import {Effect } from "effect"classHttpError {readonly_tag = "HttpError"}constprogram =Effect .fail (newHttpError ())
We use a class to represent the HttpError
type above simply to gain access
to both the error type and a free constructor. However, you can use whatever
you like to model your error types.
It's worth noting that we added a readonly _tag
field as discriminant to our error in the example:
ts
class HttpError {readonly _tag = "HttpError"}
ts
class HttpError {readonly _tag = "HttpError"}
Adding a discriminant field, such as _tag
, can be beneficial for
distinguishing between different types of errors during error handling. It
also prevents TypeScript from unifying types, ensuring that each error is
treated uniquely based on its discriminant value.
Expected errors are tracked at the type level by the Effect
data type in the "Error" channel.
It is evident from the type of program
that can fail with an error of type HttpError
:
ts
Effect<never, HttpError, never>
ts
Effect<never, HttpError, never>
Error Tracking
The following program serves as an illustration of how errors are automatically tracked for you:
ts
import {Effect ,Random } from "effect"export classFooError {readonly_tag = "FooError"}export classBarError {readonly_tag = "BarError"}export constprogram =Effect .gen (function* (_ ) {constn1 = yield*_ (Random .next )constn2 = yield*_ (Random .next )constfoo =n1 > 0.5 ? "yay!" : yield*_ (Effect .fail (newFooError ()))constbar =n2 > 0.5 ? "yay!" : yield*_ (Effect .fail (newBarError ()))returnfoo +bar })Effect .runPromise (program ).then (console .log ,console .error )
ts
import {Effect ,Random } from "effect"export classFooError {readonly_tag = "FooError"}export classBarError {readonly_tag = "BarError"}export constprogram =Effect .gen (function* (_ ) {constn1 = yield*_ (Random .next )constn2 = yield*_ (Random .next )constfoo =n1 > 0.5 ? "yay!" : yield*_ (Effect .fail (newFooError ()))constbar =n2 > 0.5 ? "yay!" : yield*_ (Effect .fail (newBarError ()))returnfoo +bar })Effect .runPromise (program ).then (console .log ,console .error )
In the above program, we compute two values: foo
and bar
, each representing a potential source of error.
Effect automatically keeps track of the possible errors that can occur during the execution of the program.
In this case, we have FooError
and BarError
as the possible error types.
The error channel of the program
is specified as
ts
Effect<string, FooError | BarError, never>
ts
Effect<string, FooError | BarError, never>
indicating that it can potentially fail with either a FooError
or a BarError
.
Short-Circuiting
When working with APIs like Effect.gen
, Effect.map
, Effect.flatMap
, and Effect.all
, 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.
ts
import {Effect ,Console } from "effect"// Define three effects representing different tasks.consttask1 =Console .log ("Executing task1...")consttask2 =Effect .fail ("Something went wrong!")consttask3 =Console .log ("Executing task3...")// Compose the three tasks to run them in sequence.// If one of the tasks fails, the subsequent tasks won't be executed.constprogram =Effect .gen (function* (_ ) {yield*_ (task1 )yield*_ (task2 ) // After task1, task2 is executed, but it fails with an erroryield*_ (task3 ) // This computation won't be executed because the previous one fails})Effect .runPromiseExit (program ).then (console .log )/*Output:Executing task1...{_id: 'Exit',_tag: 'Failure',cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong!' }}*/
ts
import {Effect ,Console } from "effect"// Define three effects representing different tasks.consttask1 =Console .log ("Executing task1...")consttask2 =Effect .fail ("Something went wrong!")consttask3 =Console .log ("Executing task3...")// Compose the three tasks to run them in sequence.// If one of the tasks fails, the subsequent tasks won't be executed.constprogram =Effect .gen (function* (_ ) {yield*_ (task1 )yield*_ (task2 ) // After task1, task2 is executed, but it fails with an erroryield*_ (task3 ) // This computation won't be executed because the previous one fails})Effect .runPromiseExit (program ).then (console .log )/*Output:Executing task1...{_id: 'Exit',_tag: 'Failure',cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong!' }}*/
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 converts an Effect<A, E, R>
into another effect where both its failure (E
) and success (A
) channels have been lifted into an Either<A, E>
data type:
ts
Effect<A, E, R> -> Effect<Either<A, E>, never, R>
ts
Effect<A, E, R> -> Effect<Either<A, E>, never, R>
The Either<R, L>
data type represents a value that can be either a Right
value (R
) or a Left
value (L
).
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.
ts
import {Effect ,Either } from "effect"import {program } from "./error-tracking"constrecovered =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (program ))if (Either .isLeft (failureOrSuccess )) {// failure case: you can extract the error from the `left` propertyconsterror =failureOrSuccess .left return `Recovering from ${error ._tag }`} else {// success case: you can extract the value from the `right` propertyreturnfailureOrSuccess .right }})
ts
import {Effect ,Either } from "effect"import {program } from "./error-tracking"constrecovered =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (program ))if (Either .isLeft (failureOrSuccess )) {// failure case: you can extract the error from the `left` propertyconsterror =failureOrSuccess .left return `Recovering from ${error ._tag }`} else {// success case: you can extract the value from the `right` propertyreturnfailureOrSuccess .right }})
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:
ts
import {Effect ,Either } from "effect"import {program } from "./error-tracking"constrecovered =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (program ))returnEither .match (failureOrSuccess , {onLeft : (error ) => `Recovering from ${error ._tag }`,onRight : (value ) =>value // do nothing in case of success})})
ts
import {Effect ,Either } from "effect"import {program } from "./error-tracking"constrecovered =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (program ))returnEither .match (failureOrSuccess , {onLeft : (error ) => `Recovering from ${error ._tag }`,onRight : (value ) =>value // do nothing in case of success})})
catchAll
The Effect.catchAll
function allows you to catch any error that occurs in the program and provide a fallback.
ts
import {Effect } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchAll ((error ) =>Effect .succeed (`Recovering from ${error ._tag }`)))
ts
import {Effect } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchAll ((error ) =>Effect .succeed (`Recovering from ${error ._tag }`)))
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
Suppose we want to handle a specific error, such as FooError
.
ts
import {Effect ,Either } from "effect"import {program } from "./error-tracking"constrecovered =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (program ))if (Either .isLeft (failureOrSuccess )) {consterror =failureOrSuccess .left if (error ._tag === "FooError") {return "Recovering from FooError"}return yield*_ (Effect .fail (error ))} else {returnfailureOrSuccess .right }})
ts
import {Effect ,Either } from "effect"import {program } from "./error-tracking"constrecovered =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (program ))if (Either .isLeft (failureOrSuccess )) {consterror =failureOrSuccess .left if (error ._tag === "FooError") {return "Recovering from FooError"}return yield*_ (Effect .fail (error ))} else {returnfailureOrSuccess .right }})
We can observe that the type in the error channel of our program has changed to only show BarError
,
indicating that FooError
has been handled.
If we also want to handle BarError
, we can easily add another case to our code:
ts
import {Effect ,Either } from "effect"import {program } from "./error-tracking"constrecovered =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (program ))if (Either .isLeft (failureOrSuccess )) {consterror =failureOrSuccess .left if (error ._tag === "FooError") {return "Recovering from FooError"} else {return "Recovering from BarError"}} else {returnfailureOrSuccess .right }})
ts
import {Effect ,Either } from "effect"import {program } from "./error-tracking"constrecovered =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (program ))if (Either .isLeft (failureOrSuccess )) {consterror =failureOrSuccess .left if (error ._tag === "FooError") {return "Recovering from FooError"} else {return "Recovering from BarError"}} else {returnfailureOrSuccess .right }})
We can observe that the type in the error channel of our program 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:
ts
import {Effect ,Option } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchSome ((error ) => {if (error ._tag === "FooError") {returnOption .some (Effect .succeed ("Recovering from FooError"))}returnOption .none ()}))
ts
import {Effect ,Option } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchSome ((error ) => {if (error ._tag === "FooError") {returnOption .some (Effect .succeed ("Recovering from FooError"))}returnOption .none ()}))
In the code above, Effect.catchSome
takes a function that examines the error (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 (recovered
in this case) will still have the same error type (FooError | BarError
) as the original effect.
catchIf
Similar to Effect.catchSome
, the function Effect.catchIf
allows you to recover from specific errors based on a predicate:
ts
import {Effect } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchIf ((error ) =>error ._tag === "FooError",() =>Effect .succeed ("Recovering from FooError")))
ts
import {Effect } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchIf ((error ) =>error ._tag === "FooError",() =>Effect .succeed ("Recovering from FooError")))
It's important to note that while Effect.catchIf
lets you catch specific errors, it doesn't alter the error type itself. Therefore, the resulting effect (recovered
in this case) will still have the same error type (FooError | BarError
) as the original effect.
However, if you provide a user-defined type guard instead of a predicate, the resulting error type will be pruned, returning an Effect<string, BarError, never>
:
ts
import {Effect } from "effect"import {program ,FooError } from "./error-tracking"constrecovered =program .pipe (Effect .catchIf ((error ):error isFooError =>error ._tag === "FooError",() =>Effect .succeed ("Recovering from FooError")))
ts
import {Effect } from "effect"import {program ,FooError } from "./error-tracking"constrecovered =program .pipe (Effect .catchIf ((error ):error isFooError =>error ._tag === "FooError",() =>Effect .succeed ("Recovering from FooError")))
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.
ts
import {Effect } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchTag ("FooError", (_fooError ) =>Effect .succeed ("Recovering from FooError")))
ts
import {Effect } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchTag ("FooError", (_fooError ) =>Effect .succeed ("Recovering from FooError")))
In the example above, the Effect.catchTag
function allows us to handle FooError
specifically.
If a FooError
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 BarError
,
indicating that FooError
has been handled.
If we also wanted to handle BarError
, we can simply add another catchTag
:
ts
import {Effect } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchTag ("FooError", (_fooError ) =>Effect .succeed ("Recovering from FooError")),Effect .catchTag ("BarError", (_barError ) =>Effect .succeed ("Recovering from BarError")))
ts
import {Effect } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchTag ("FooError", (_fooError ) =>Effect .succeed ("Recovering from FooError")),Effect .catchTag ("BarError", (_barError ) =>Effect .succeed ("Recovering from BarError")))
We can observe that the type in the error channel of our program has changed to never
,
indicating that all errors have been handled.
It is important to ensure that the error type used with catchTag
has a
readonly _tag
discriminant field. This field is required for the matching
and handling of specific error tags.
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.
ts
import {Effect } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchTags ({FooError : (_fooError ) =>Effect .succeed (`Recovering from FooError`),BarError : (_barError ) =>Effect .succeed (`Recovering from BarError`)}))
ts
import {Effect } from "effect"import {program } from "./error-tracking"constrecovered =program .pipe (Effect .catchTags ({FooError : (_fooError ) =>Effect .succeed (`Recovering from FooError`),BarError : (_barError ) =>Effect .succeed (`Recovering from BarError`)}))
In the above example, instead of using Effect.catchTag
multiple times to handle individual errors, we utilize the Effect.catchTags
combinator.
This combinator takes an object where each property represents a specific error _tag
("FooError"
and "BarError"
in this case),
and the corresponding value is the error handler function to be executed when that particular error occurs.
It is important to ensure that all the error types used with
Effect.catchTags
have a readonly _tag
discriminant field. This field is
required for the matching and handling of specific error tags.