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 .andThen ((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 .andThen ((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 developers to observe failures or underlying issues without modifying the outcome:
Function | Description |
---|---|
tapError | Executes an effectful operation to inspect the failure of an effect without altering it. |
tapErrorTag | Specifically inspects a failure with a particular tag, allowing focused error handling. |
tapErrorCause | Inspects the underlying cause of an effect's failure. |
tapDefect | Specifically inspects non-recoverable failures or defects in an effect (i.e., one or more Die causes). |
tapBoth | Inspects both success and failure outcomes of an effect, performing different actions based on the result. |
Utilizing these error inspection tools does not alter the outcome or type of the effect.
tapError
Executes an effectful operation to inspect the failure of an effect without altering it.
ts
import {Effect ,Console } from "effect"// Create an effect that is designed to fail, simulating an occurrence of a network errorconsttask :Effect .Effect <number, string> =Effect .fail ("NetworkError")// Log the error message if the task fails. This function only executes if there is an error,// providing a method to handle or inspect errors without altering the outcome of the original effect.consttapping =Effect .tapError (task , (error ) =>Console .log (`expected error: ${error }`))Effect .runFork (tapping )/*Output:expected error: NetworkError*/
ts
import {Effect ,Console } from "effect"// Create an effect that is designed to fail, simulating an occurrence of a network errorconsttask :Effect .Effect <number, string> =Effect .fail ("NetworkError")// Log the error message if the task fails. This function only executes if there is an error,// providing a method to handle or inspect errors without altering the outcome of the original effect.consttapping =Effect .tapError (task , (error ) =>Console .log (`expected error: ${error }`))Effect .runFork (tapping )/*Output:expected error: NetworkError*/
tapErrorTag
Specifically inspects a failure with a particular tag, allowing focused error handling.
ts
import {Effect ,Console } from "effect"classNetworkError {readonly_tag = "NetworkError"constructor(readonlystatusCode : number) {}}classValidationError {readonly_tag = "ValidationError"constructor(readonlyfield : string) {}}// Create an effect that is designed to fail, simulating an occurrence of a network errorconsttask :Effect .Effect <number,NetworkError |ValidationError > =Effect .fail (newNetworkError (504))// Apply an error handling function only to errors tagged as "NetworkError",// and log the corresponding status code of the error.consttapping =Effect .tapErrorTag (task , "NetworkError", (error ) =>Console .log (`expected error: ${error .statusCode }`))Effect .runFork (tapping )/*Output:expected error: 504*/
ts
import {Effect ,Console } from "effect"classNetworkError {readonly_tag = "NetworkError"constructor(readonlystatusCode : number) {}}classValidationError {readonly_tag = "ValidationError"constructor(readonlyfield : string) {}}// Create an effect that is designed to fail, simulating an occurrence of a network errorconsttask :Effect .Effect <number,NetworkError |ValidationError > =Effect .fail (newNetworkError (504))// Apply an error handling function only to errors tagged as "NetworkError",// and log the corresponding status code of the error.consttapping =Effect .tapErrorTag (task , "NetworkError", (error ) =>Console .log (`expected error: ${error .statusCode }`))Effect .runFork (tapping )/*Output:expected error: 504*/
tapErrorCause
Inspects the underlying cause of an effect's failure.
ts
import { Effect, Console } from "effect"// Create an effect that is designed to fail, simulating an occurrence of a network errorconst task1: Effect.Effect<number, string> = Effect.fail("NetworkError")// This will log the cause of any expected error or defectconst tapping1 = Effect.tapErrorCause(task1, (cause) =>Console.log(`error cause: ${cause}`))Effect.runFork(tapping1)/*Output:error cause: Error: NetworkError*/// Simulate a severe failure in the system by causing a defect with a specific message.const task2: Effect.Effect<number, string> = Effect.dieMessage("Something went wrong")// This will log the cause of any expected error or defectconst tapping2 = Effect.tapErrorCause(task2, (cause) =>Console.log(`error cause: ${cause}`))Effect.runFork(tapping2)/*Output:error cause: RuntimeException: Something went wrong... stack trace ...*/
ts
import { Effect, Console } from "effect"// Create an effect that is designed to fail, simulating an occurrence of a network errorconst task1: Effect.Effect<number, string> = Effect.fail("NetworkError")// This will log the cause of any expected error or defectconst tapping1 = Effect.tapErrorCause(task1, (cause) =>Console.log(`error cause: ${cause}`))Effect.runFork(tapping1)/*Output:error cause: Error: NetworkError*/// Simulate a severe failure in the system by causing a defect with a specific message.const task2: Effect.Effect<number, string> = Effect.dieMessage("Something went wrong")// This will log the cause of any expected error or defectconst tapping2 = Effect.tapErrorCause(task2, (cause) =>Console.log(`error cause: ${cause}`))Effect.runFork(tapping2)/*Output:error cause: RuntimeException: Something went wrong... stack trace ...*/
tapDefect
Specifically inspects non-recoverable failures or defects in an effect (i.e., one or more Die causes).
ts
import { Effect, Console } from "effect"// Create an effect that is designed to fail, simulating an occurrence of a network errorconst task1: Effect.Effect<number, string> = Effect.fail("NetworkError")// this won't log anything because is not a defectconst tapping1 = Effect.tapDefect(task1, (cause) =>Console.log(`defect: ${cause}`))Effect.runFork(tapping1)/*No Output*/// Simulate a severe failure in the system by causing a defect with a specific message.const task2: Effect.Effect<number, string> = Effect.dieMessage("Something went wrong")// This will only log defects, not errorsconst tapping2 = Effect.tapDefect(task2, (cause) =>Console.log(`defect: ${cause}`))Effect.runFork(tapping2)/*Output:defect: RuntimeException: Something went wrong... stack trace ...*/
ts
import { Effect, Console } from "effect"// Create an effect that is designed to fail, simulating an occurrence of a network errorconst task1: Effect.Effect<number, string> = Effect.fail("NetworkError")// this won't log anything because is not a defectconst tapping1 = Effect.tapDefect(task1, (cause) =>Console.log(`defect: ${cause}`))Effect.runFork(tapping1)/*No Output*/// Simulate a severe failure in the system by causing a defect with a specific message.const task2: Effect.Effect<number, string> = Effect.dieMessage("Something went wrong")// This will only log defects, not errorsconst tapping2 = Effect.tapDefect(task2, (cause) =>Console.log(`defect: ${cause}`))Effect.runFork(tapping2)/*Output:defect: RuntimeException: Something went wrong... stack trace ...*/
tapBoth
Inspects both success and failure outcomes of an effect, performing different actions based on the result.
ts
import {Effect ,Random ,Console } from "effect"// Simulate an effect that might failconsttask =Effect .filterOrFail (Random .nextRange (-1, 1),(n ) =>n >= 0,() => "random number is negative")// Define an effect that logs both success and failure outcomes of the 'task'consttapping =Effect .tapBoth (task , {onFailure : (error ) =>Console .log (`failure: ${error }`),onSuccess : (randomNumber ) =>Console .log (`random number: ${randomNumber }`)})Effect .runFork (tapping )/*Example Output:failure: random number is negative*/
ts
import {Effect ,Random ,Console } from "effect"// Simulate an effect that might failconsttask =Effect .filterOrFail (Random .nextRange (-1, 1),(n ) =>n >= 0,() => "random number is negative")// Define an effect that logs both success and failure outcomes of the 'task'consttapping =Effect .tapBoth (task , {onFailure : (error ) =>Console .log (`failure: ${error }`),onSuccess : (randomNumber ) =>Console .log (`random number: ${randomNumber }`)})Effect .runFork (tapping )/*Example Output:failure: random number is negative*/
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 )