As you continue to adopt Effect into your codebase, you may encounter situations where you need to propagate error information from Effect code to non-Effect code.
For example, perhaps your business requirements dictate that you must log all errors to a third-party log aggregator.
In this section, we will explore several strategies for allowing your existing application to access both expected and unexpected errors coming from Effect code.
For more information about managing errors within your Effect programs, checkout the section on Error Management in the documentation.
Let's imagine that we are working on refactoring a part of our application to use Effect. The part of our application we are refactoring involves communicating with one of our company's REST APIs to retrieve the price of a particular product we sell. If requests to our company's API are unsuccessful, the program will fail with an HttpError
.
Our business requirements dictate the following:
ErrorReporter
must be used to report all errors occurring within our application to an external error tracking serviceLogAggregator
must be used to log the results of successful API calls to an external log aggregation serviceEffect provides a variety of tools to make working with expected errors in your program as simple as possible. These tools can also be used to provide the surrounding application with error information coming from your Effect code.
In the first example below, we utilize Effect.catchTag
to "catch" the HttpError
in our Effect program. We then use Effect.promise
to allow our application's ErrorReporter
to report on the error.
ts
import {Data ,Effect } from "effect"// === Effect Code ===classHttpError extendsData .TaggedError ("HttpError") {}constprogram =Effect .gen (function*() {// Simulate the possibility of errorif (Math .random () > 0.5) {return yield* newHttpError ()}// Simulate a response from our APIreturn yield*Effect .succeed (42)})// === Application Code ===interfaceLogAggregator {log (value : any):Promise <void>}interfaceErrorReporter {report (error : any):Promise <void>}declare constlogger :LogAggregator declare constreporter :ErrorReporter async functionmain () {awaitprogram .pipe (Effect .andThen ((result ) =>logger .log (result )),Effect .catchTag ("HttpError", (error ) =>Effect .promise (() =>reporter .report (error ))),Effect .runPromise )}
ts
import {Data ,Effect } from "effect"// === Effect Code ===classHttpError extendsData .TaggedError ("HttpError") {}constprogram =Effect .gen (function*() {// Simulate the possibility of errorif (Math .random () > 0.5) {return yield* newHttpError ()}// Simulate a response from our APIreturn yield*Effect .succeed (42)})// === Application Code ===interfaceLogAggregator {log (value : any):Promise <void>}interfaceErrorReporter {report (error : any):Promise <void>}declare constlogger :LogAggregator declare constreporter :ErrorReporter async functionmain () {awaitprogram .pipe (Effect .andThen ((result ) =>logger .log (result )),Effect .catchTag ("HttpError", (error ) =>Effect .promise (() =>reporter .report (error ))),Effect .runPromise )}
In the next example below, we wrap our entire program in a call to Effect.either
. This operator will convert an Effect<A, E, R>
into an Effect<Either<A, E>, never, R>
, thereby exposing expected errors in the success channel of your program.
We can then use methods from the Either
module to handle error and success cases separately.
ts
import {Data ,Effect ,Either } from "effect"// === Effect Code ===classHttpError extendsData .TaggedError ("HttpError") {}constprogram =Effect .gen (function*() {// Simulate the possibility of errorif (Math .random () > 0.5) {return yield* newHttpError ()}return yield*Effect .succeed (42)})// === Application Code ===interfaceLogAggregator {log (value : any):Promise <void>}interfaceErrorReporter {report (error : any):Promise <void>}declare constlogger :LogAggregator declare constreporter :ErrorReporter async functionmain () {awaitEffect .runPromise (Effect .either (program )).then (Either .match ({// Handle the error caseonLeft : (error ) =>reporter .report (error ),// Handle the success caseonRight : (value ) =>logger .log (value )}))}
ts
import {Data ,Effect ,Either } from "effect"// === Effect Code ===classHttpError extendsData .TaggedError ("HttpError") {}constprogram =Effect .gen (function*() {// Simulate the possibility of errorif (Math .random () > 0.5) {return yield* newHttpError ()}return yield*Effect .succeed (42)})// === Application Code ===interfaceLogAggregator {log (value : any):Promise <void>}interfaceErrorReporter {report (error : any):Promise <void>}declare constlogger :LogAggregator declare constreporter :ErrorReporter async functionmain () {awaitEffect .runPromise (Effect .either (program )).then (Either .match ({// Handle the error caseonLeft : (error ) =>reporter .report (error ),// Handle the success caseonRight : (value ) =>logger .log (value )}))}
The simplest method of gaining access to both expected and unexpected errors returned from an Effect program is to simply run your program with Effect.runPromise
and then catch any thrown errors.
ts
import {Data ,Effect } from "effect"// === Effect Code ===classHttpError extendsData .TaggedError ("HttpError") {}constprogram =Effect .gen (function*() {// Simulate the possibility of errorif (Math .random () > 0.5) {return yield* newHttpError ()}return yield*Effect .succeed (42)})// === Application Code ===interfaceLogAggregator {log (value : any):Promise <void>}interfaceErrorReporter {report (error : any):Promise <void>}declare constlogger :LogAggregator declare constreporter :ErrorReporter async functionmain () {awaitEffect .runPromise (program ).then ((value ) =>logger .log (value )).catch ((error ) =>reporter .report (error ))}
ts
import {Data ,Effect } from "effect"// === Effect Code ===classHttpError extendsData .TaggedError ("HttpError") {}constprogram =Effect .gen (function*() {// Simulate the possibility of errorif (Math .random () > 0.5) {return yield* newHttpError ()}return yield*Effect .succeed (42)})// === Application Code ===interfaceLogAggregator {log (value : any):Promise <void>}interfaceErrorReporter {report (error : any):Promise <void>}declare constlogger :LogAggregator declare constreporter :ErrorReporter async functionmain () {awaitEffect .runPromise (program ).then ((value ) =>logger .log (value )).catch ((error ) =>reporter .report (error ))}
The benefit of this approach is that errors returned by Effect.runPromise
will be automatically wrapped by Effect's special FiberFailure
error. The FiberFailure
error type will prettify the Cause
of the failure for you (see the Cause documentation for more information), making it easier to log the error and any associated stack trace.
However, there are several downsides to this approach:
Cause
of the failure for additional informationCause
, only the first error will be rendered by the FiberFailure
Introspecting the full Cause
of failure of our Effect programs can be extremely useful when we need granular information about the failure(s) that occurred.
For example, perhaps we need to respond to an Interrupt
cause in a different manner than a Fail
cause.
If we want to perform some operations on the full Cause
of failure of our program, we have a variety options available to us:
Cause
of failure in our program's error channelEffect.runPromiseExit
and then match on the success and error casesThe team in charge of the TodoRepository
has continued to refactor the create
method. The method now returns an Effect<Todo, CreateTodoError>
.
Using what we have learned above, your tasks for this exercise include:
Todo
results in a CreateTodoError
404
{ "type": "CreateTodoError", "text": <TODO_TEXT> }
Modify the code in the editor to the right to accomplish your tasks.
Please note that there is no one "correct" answer, as there are multiple ways to achieve the desired outcome.