Error Accumulation
On this page
Sequential combinators such as Effect.zip
and Effect.forEach
have a "fail fast" policy when it comes to error management. This means that they stop and return immediately when they encounter the first error.
Let's take a look at an example using the Effect.zip
operator. In this example, we can see that Effect.zip
will fail as soon as it encounters the first failure. As a result, only the first error is displayed:
ts
import {Effect } from "effect"consttask1 =Effect .succeed (1)consttask2 =Effect .fail ("Oh uh!").pipe (Effect .as (2))consttask3 =Effect .succeed (3)consttask4 =Effect .fail ("Oh no!").pipe (Effect .as (4))constprogram =task1 .pipe (Effect .zip (task2 ),Effect .zip (task3 ),Effect .zip (task4 ))Effect .runPromise (program ).then (console .log ,console .error )/*Output:{_id: 'FiberFailure',cause: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' }}*/
ts
import {Effect } from "effect"consttask1 =Effect .succeed (1)consttask2 =Effect .fail ("Oh uh!").pipe (Effect .as (2))consttask3 =Effect .succeed (3)consttask4 =Effect .fail ("Oh no!").pipe (Effect .as (4))constprogram =task1 .pipe (Effect .zip (task2 ),Effect .zip (task3 ),Effect .zip (task4 ))Effect .runPromise (program ).then (console .log ,console .error )/*Output:{_id: 'FiberFailure',cause: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' }}*/
The Effect.forEach
function behaves similarly. It takes a collection and an effectful operation, and tries to apply the operation to all elements of the collection. However, it also follows the "fail fast" policy and fails when it encounters the first error:
ts
import {Effect } from "effect"constprogram =Effect .forEach ([1, 2, 3, 4, 5], (n ) => {if (n < 4) {returnEffect .succeed (n )} else {returnEffect .fail (`${n } is not less that 4`)}})Effect .runPromise (program ).then (console .log ,console .error )/*Output:{_id: 'FiberFailure',cause: { _id: 'Cause', _tag: 'Fail', failure: '4 is not less that 4' }}*/
ts
import {Effect } from "effect"constprogram =Effect .forEach ([1, 2, 3, 4, 5], (n ) => {if (n < 4) {returnEffect .succeed (n )} else {returnEffect .fail (`${n } is not less that 4`)}})Effect .runPromise (program ).then (console .log ,console .error )/*Output:{_id: 'FiberFailure',cause: { _id: 'Cause', _tag: 'Fail', failure: '4 is not less that 4' }}*/
However, there are situations where we may need to collect all potential errors in a computation instead of failing fast. In such cases, we can use operators that accumulate errors as well as successes.
validate
The Effect.validate
function is similar to Effect.zip
, but if it encounters an error, it continues the zip operation instead of stopping. It combines the effects and accumulates both errors and successes:
ts
import {Effect } from "effect"consttask1 =Effect .succeed (1)consttask2 =Effect .fail ("Oh uh!").pipe (Effect .as (2))consttask3 =Effect .succeed (3)consttask4 =Effect .fail ("Oh no!").pipe (Effect .as (4))constprogram =task1 .pipe (Effect .validate (task2 ),Effect .validate (task3 ),Effect .validate (task4 ))Effect .runPromise (program ).then (console .log ,console .error )/*Output:{_id: 'FiberFailure',cause: {_id: 'Cause',_tag: 'Sequential',left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' },right: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' }}}*/
ts
import {Effect } from "effect"consttask1 =Effect .succeed (1)consttask2 =Effect .fail ("Oh uh!").pipe (Effect .as (2))consttask3 =Effect .succeed (3)consttask4 =Effect .fail ("Oh no!").pipe (Effect .as (4))constprogram =task1 .pipe (Effect .validate (task2 ),Effect .validate (task3 ),Effect .validate (task4 ))Effect .runPromise (program ).then (console .log ,console .error )/*Output:{_id: 'FiberFailure',cause: {_id: 'Cause',_tag: 'Sequential',left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' },right: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' }}}*/
With Effect.validate
, we can collect all the errors encountered during the computation instead of stopping at the first error. This allows us to have a complete picture of all the potential errors and successes in our program.
validateAll
The Effect.validateAll
function is similar to the Effect.forEach
function. It transforms all elements of a collection using the provided effectful operation, but it collects all errors in the error channel, as well as the success values in the success channel.
ts
import {Effect } from "effect"constprogram =Effect .validateAll ([1, 2, 3, 4, 5], (n ) => {if (n < 4) {returnEffect .succeed (n )} else {returnEffect .fail (`${n } is not less that 4`)}})Effect .runPromise (program ).then (console .log ,console .error )/*Output:{_id: 'FiberFailure',cause: {_id: 'Cause',_tag: 'Fail',failure: [ '4 is not less that 4', '5 is not less that 4' ]}}*/
ts
import {Effect } from "effect"constprogram =Effect .validateAll ([1, 2, 3, 4, 5], (n ) => {if (n < 4) {returnEffect .succeed (n )} else {returnEffect .fail (`${n } is not less that 4`)}})Effect .runPromise (program ).then (console .log ,console .error )/*Output:{_id: 'FiberFailure',cause: {_id: 'Cause',_tag: 'Fail',failure: [ '4 is not less that 4', '5 is not less that 4' ]}}*/
Note that this function is lossy, which means that if there are any errors, all successes will be lost. If you need a function that preserves both successes and failures, please refer to the partition function.
validateFirst
The Effect.validateFirst
function is similar to Effect.validateAll
but it will return the first success (or all the failures):
ts
import {Effect } from "effect"constprogram =Effect .validateFirst ([1, 2, 3, 4, 5], (n ) => {if (n < 4) {returnEffect .fail (`${n } is not less that 4`)} else {returnEffect .succeed (n )}})Effect .runPromise (program ).then (console .log ,console .error )// Output: 4
ts
import {Effect } from "effect"constprogram =Effect .validateFirst ([1, 2, 3, 4, 5], (n ) => {if (n < 4) {returnEffect .fail (`${n } is not less that 4`)} else {returnEffect .succeed (n )}})Effect .runPromise (program ).then (console .log ,console .error )// Output: 4
Please note that the return type is number
instead of number[]
, as in the case of validateAll
.
partition
The Effect.partition
function takes an iterable and an effectful function that transforms each value of the iterable. It then creates a tuple of both failures and successes in the success channel:
ts
import {Effect } from "effect"constprogram =Effect .partition ([0, 1, 2, 3, 4], (n ) => {if (n % 2 === 0) {returnEffect .succeed (n )} else {returnEffect .fail (`${n } is not even`)}})Effect .runPromise (program ).then (console .log ,console .error )// Output: [ [ '1 is not even', '3 is not even' ], [ 0, 2, 4 ] ]
ts
import {Effect } from "effect"constprogram =Effect .partition ([0, 1, 2, 3, 4], (n ) => {if (n % 2 === 0) {returnEffect .succeed (n )} else {returnEffect .fail (`${n } is not even`)}})Effect .runPromise (program ).then (console .log ,console .error )// Output: [ [ '1 is not even', '3 is not even' ], [ 0, 2, 4 ] ]
Please note that this operator is an unexceptional effect, which means that the type of the error channel is never
. Therefore, if we encounter a failure case, the whole effect doesn't fail.