Error Channel Operations
On this page
In Effect you can perform various operations on the error channel of effects. These operations allow you to transform, inspect, and handle errors in different ways. Let's explore some of these operations.
Map Operations
mapError
The Effect.mapError
function is used when you need to transform or modify an error produced by an effect, without affecting the success value. This can be helpful when you want to add extra information to the error or change its type.
ts
import {Effect } from "effect"constsimulatedTask =Effect .fail ("Oh no!").pipe (Effect .as (1))constmapped =Effect .mapError (simulatedTask , (message ) => newError (message ))
ts
import {Effect } from "effect"constsimulatedTask =Effect .fail ("Oh no!").pipe (Effect .as (1))constmapped =Effect .mapError (simulatedTask , (message ) => newError (message ))
We can observe that the type in the error channel of our program has changed from string
to Error
.
It's important to note that using the Effect.mapError
function does not
change the overall success or failure of the effect. If the mapped effect
is successful, then the mapping function is ignored. In other words, the
Effect.mapError
operation only applies the transformation to the error
channel of the effect, while leaving the success channel unchanged.
mapBoth
The Effect.mapBoth
function allows you to apply transformations to both channels: the error channel and the success channel of an effect. It takes two map functions as arguments: one for the error channel and the other for the success channel.
ts
import {Effect } from "effect"constsimulatedTask =Effect .fail ("Oh no!").pipe (Effect .as (1))constmodified =Effect .mapBoth (simulatedTask , {onFailure : (message ) => newError (message ),onSuccess : (n ) =>n > 0})
ts
import {Effect } from "effect"constsimulatedTask =Effect .fail ("Oh no!").pipe (Effect .as (1))constmodified =Effect .mapBoth (simulatedTask , {onFailure : (message ) => newError (message ),onSuccess : (n ) =>n > 0})
After using mapBoth
, we can observe that the type of our program has changed from Effect<number, string>
to Effect<boolean, Error>
.
It's important to note that using the mapBoth
function does not change
the overall success or failure of the effect. It only transforms the values
in the error and success channels while preserving the effect's original
success or failure status.
Filtering the Success Channel
The Effect library provides several operators to filter values on the success channel based on a given predicate. These operators offer different strategies for handling cases where the predicate fails. Let's explore them:
Function | Description |
---|---|
Effect.filterOrFail | This operator filters the values on the success channel based on a predicate. If the predicate fails for any value, the original effect fails with an error. |
Effect.filterOrDie and Effect.filterOrDieMessage | These operators also filter the values on the success channel based on a predicate. If the predicate fails for any value, the original effect terminates abruptly. The filterOrDieMessage variant allows you to provide a custom error message. |
Effect.filterOrElse | This operator filters the values on the success channel based on a predicate. If the predicate fails for any value, an alternative effect is executed instead. |
Here's an example that demonstrates these filtering operators in action:
ts
import {Effect ,Random ,Cause } from "effect"consttask1 =Effect .filterOrFail (Random .nextRange (-1, 1),(n ) =>n >= 0,() => "random number is negative")consttask2 =Effect .filterOrDie (Random .nextRange (-1, 1),(n ) =>n >= 0,() => newCause .IllegalArgumentException ("random number is negative"))consttask3 =Effect .filterOrDieMessage (Random .nextRange (-1, 1),(n ) =>n >= 0,"random number is negative")consttask4 =Effect .filterOrElse (Random .nextRange (-1, 1),(n ) =>n >= 0,() =>task3 )
ts
import {Effect ,Random ,Cause } from "effect"consttask1 =Effect .filterOrFail (Random .nextRange (-1, 1),(n ) =>n >= 0,() => "random number is negative")consttask2 =Effect .filterOrDie (Random .nextRange (-1, 1),(n ) =>n >= 0,() => newCause .IllegalArgumentException ("random number is negative"))consttask3 =Effect .filterOrDieMessage (Random .nextRange (-1, 1),(n ) =>n >= 0,"random number is negative")consttask4 =Effect .filterOrElse (Random .nextRange (-1, 1),(n ) =>n >= 0,() =>task3 )
It's important to note that depending on the specific filtering operator used, the effect can either fail, terminate abruptly, or execute an alternative effect when the predicate fails. Choose the appropriate operator based on your desired error handling strategy and program logic.
In addition to the filtering capabilities discussed earlier, you have the option to further refine and narrow down the type of the success channel by providing a user-defined type guard to the filterOr*
APIs. This not only enhances type safety but also improves code clarity. Let's explore this concept through an example:
ts
import {Effect ,pipe } from "effect"// Define a user interfaceinterfaceUser {readonlyname : string}// Assume an asynchronous authentication functiondeclare constauth : () =>Promise <User | null>constprogram =pipe (Effect .promise (() =>auth ()),Effect .filterOrFail (// Define a guard to narrow down the type(user ):user isUser =>user !== null,() => newError ("Unauthorized")),Effect .map ((user ) =>user .name ) // The 'user' here has type `User`, not `User | null`)
ts
import {Effect ,pipe } from "effect"// Define a user interfaceinterfaceUser {readonlyname : string}// Assume an asynchronous authentication functiondeclare constauth : () =>Promise <User | null>constprogram =pipe (Effect .promise (() =>auth ()),Effect .filterOrFail (// Define a guard to narrow down the type(user ):user isUser =>user !== null,() => newError ("Unauthorized")),Effect .map ((user ) =>user .name ) // The 'user' here has type `User`, not `User | null`)
In the example above, a guard is used within the filterOrFail
API to ensure that the user
is of type User
rather than User | null
. This refined type information improves the reliability of your code and makes it more understandable.
If you prefer, you can utilize a pre-made guard like Predicate.isNotNull
for simplicity and consistency.
Inspecting Errors
Similar to tapping for success values, Effect provides several operators for inspecting error values. These operators allow us to peek into failures or underlying defects or causes:
tapError
tapBoth
tapErrorCause
tapDefect
Let's see an example of how to use these operators:
ts
import {Effect ,Random ,Console } from "effect"consttask =Effect .filterOrFail (Random .nextRange (-1, 1),(n ) =>n >= 0,() => "random number is negative")consttapping1 =Effect .tapError (task , (error ) =>Console .log (`failure: ${error }`))consttapping2 =Effect .tapBoth (task , {onFailure : (error ) =>Console .log (`failure: ${error }`),onSuccess : (randomNumber ) =>Console .log (`random number: ${randomNumber }`)})
ts
import {Effect ,Random ,Console } from "effect"consttask =Effect .filterOrFail (Random .nextRange (-1, 1),(n ) =>n >= 0,() => "random number is negative")consttapping1 =Effect .tapError (task , (error ) =>Console .log (`failure: ${error }`))consttapping2 =Effect .tapBoth (task , {onFailure : (error ) =>Console .log (`failure: ${error }`),onSuccess : (randomNumber ) =>Console .log (`random number: ${randomNumber }`)})
It's important to note that tapping into error values does not change the type of the program.
Exposing Errors in The Success Channel
You can use the Effect.either
function to convert 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 resulting effect is an unexceptional effect, which means it cannot fail, because the failure case has been exposed as part of the Either
left case. Therefore, the error parameter of the returned Effect is never
, as it is guaranteed that the effect does not model failure.
This function becomes especially useful when recovering from effects that may fail when using Effect.gen
.
ts
import {Effect ,Either ,Console } from "effect"constsimulatedTask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constprogram =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (simulatedTask ))if (Either .isLeft (failureOrSuccess )) {consterror =failureOrSuccess .left yield*_ (Console .log (`failure: ${error }`))return 0} else {constvalue =failureOrSuccess .right yield*_ (Console .log (`success: ${value }`))returnvalue }})
ts
import {Effect ,Either ,Console } from "effect"constsimulatedTask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constprogram =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (simulatedTask ))if (Either .isLeft (failureOrSuccess )) {consterror =failureOrSuccess .left yield*_ (Console .log (`failure: ${error }`))return 0} else {constvalue =failureOrSuccess .right yield*_ (Console .log (`success: ${value }`))returnvalue }})
Exposing the Cause in The Success Channel
You can use the Effect.cause
function to expose the cause of an effect, which is a more detailed representation of failures, including error messages and defects.
ts
import {Effect ,Console } from "effect"constsimulatedTask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constprogram =Effect .gen (function* (_ ) {constcause = yield*_ (Effect .cause (simulatedTask ))yield*_ (Console .log (cause ))})
ts
import {Effect ,Console } from "effect"constsimulatedTask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constprogram =Effect .gen (function* (_ ) {constcause = yield*_ (Effect .cause (simulatedTask ))yield*_ (Console .log (cause ))})
Merging the Error Channel into the Success Channel
Using the Effect.merge
function, you can merge the error channel into the success channel, creating an effect that always succeeds with the merged value.
ts
import {Effect } from "effect"constsimulatedTask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constmerged =Effect .merge (simulatedTask )
ts
import {Effect } from "effect"constsimulatedTask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constmerged =Effect .merge (simulatedTask )
Flipping Error and Success Channels
Using the Effect.flip
function, you can flip the error and success channels of an effect, effectively swapping their roles.
ts
import {Effect } from "effect"constsimulatedTask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constflipped =Effect .flip (simulatedTask )
ts
import {Effect } from "effect"constsimulatedTask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constflipped =Effect .flip (simulatedTask )