pipe, dual and effect succeed

We solved part of the problem. Error handling in practice is 2 steps:

  • Collecting possible errors
  • Handling errors

We did the "collection" part with tryPromise, but we still miss the "handling" part.

In fact, right now running main will still crash the app if something goes wrong:

import { Effect } from "effect";

const fetchRequest = Effect.tryPromise(() =>
  fetch("https://pokeapi.co/api/v2/pokemon/garchomp/")
);

const jsonResponse = (response: Response) =>
  Effect.tryPromise(() => response.json());

const main = Effect.flatMap(fetchRequest, jsonResponse);

/// 👇 This will throw when something goes wrong (`UnknownException`)
Effect.runPromise(main);
> effect-getting-started-course@1.0.0 dev
> tsx src/index.ts

node:internal/process/promises:289
            triggerUncaughtException(err, true /* fromPromise */);
            ^

UnknownException: An unknown error occurred
    at e

Before running the effect we write some code to define what happens if Effect contains UnknownException.

This operation is called Recovering from an error.

Composing effects with pipe

We will work a lot with functions. Wrapping functions inside each other becomes quickly unreadable.

Let's imagine we add another step to our program:

const fetchRequest = Effect.tryPromise(() =>
  fetch("https://pokeapi.co/api/v2/pokemon/garchomp/")
);

const jsonResponse = (response: Response) =>
  Effect.tryPromise(() => response.json());

const savePokemon = (pokemon: unknown) =>
  Effect.tryPromise(() =>
    fetch("/api/pokemon", { body: JSON.stringify(pokemon) })
  );

How do we compose this third step? Another flatMap looks like this:

const main = Effect.flatMap(
  Effect.flatMap(fetchRequest, jsonResponse),
  savePokemon
);

Where does the program start? In what sequence are these operations executed? Again, unreadable and unmaintainable.

Fear not! There is a better-looking solution: pipe.

import { Effect, pipe } from "effect";

const main = pipe(
  fetchRequest,
  Effect.flatMap(jsonResponse),
  Effect.flatMap(savePokemon)
);

pipe takes the result of a function and provides ("pipes") it to the next one in the chain.

const main: number = pipe(
  10,
  (num) => num.toString(), // 👈 `num` is 10
  (str) => str.length > 0, // 👈 `str` is `num.toString()`
  (bool) => Number(bool) // 👈 `bool` is `str.length > 0`
);

You can now read the program as a series of steps executed top-to-bottom.

Every parameter inside pipe is a function that provides the result of the previous function, like this:

const main = pipe(
  fetchRequest,
  (fetchRequestEffect) => Effect.flatMap(fetchRequestEffect, jsonResponse),
  (jsonResponseEffect) => Effect.flatMap(jsonResponseEffect, savePokemon)
);

Since this pattern is used everywhere, every Effect comes with its own pipe function. We can therefore improve the code even more (without importing pipe):

import { Effect } from "effect";

const main = fetchRequest.pipe(
  Effect.flatMap(jsonResponse),
  Effect.flatMap(savePokemon)
);
Effect Playground

Notice how this is similar to then with Promise:

const main = fetchRequest.then(
  (response) => jsonResponse(response).then(
    (json) => savePokemon(json)
  )
);

Effect dual API

In the previous pipe code we went from this:

const main = fetchRequest.pipe(
  (fetchRequestEffect) => Effect.flatMap(fetchRequestEffect, jsonResponse)
);

To this:

const main = fetchRequest.pipe(
  Effect.flatMap(jsonResponse)
);

What is happening here?

In effect most APIs are dual. This means that they accept both multiple or single parameters:

  • Pass multiple parameters in the same flatMap
const main = pipe(
  fetchRequest,
  (fetchRequestEffect) => Effect.flatMap(fetchRequestEffect, jsonResponse)
);
  • Pass single parameters one by one
const main = pipe(
  fetchRequest,
  // 👉 Notice how `fetchRequestEffect` and `jsonResponse` are inverted compared to before
  (fetchRequestEffect) => Effect.flatMap(jsonResponse)(fetchRequestEffect)
);

This is a (simplified) definition of flatMap:

export declare const flatMap: {
  // 👇 First the function (`jsonResponse`) and then `Effect`
  <A, B>(f: (a: A) => Effect<B>): (self: Effect<A>) => Effect<B>

  // 👇 Both `Effect` and function (`jsonResponse`) in the same `flatMap`
  <A, B>(self: Effect<A>, f: (a: A) => Effect<B>): Effect<B>
}

Passing a single parameter at the time is a technique called Partial Application.

In effect functions can be "data-first" or "data-last".

It's used to more easily compose functions:

const dataFirst = (n: number, str: string) => n + str.length;
const dataLast = (str: string) => (n: number) => n + str.length;

[1, 2, 3, 4].map((value) => dataFirst(value, "abc"));

/// 👇 Directly pass the function, no need of intermediate `value`
[1, 2, 3, 4].map(dataLast("abc"));

This allows to simplify the code to a series of readable steps:

const main = fetchRequest.pipe(
  (fetchRequestEffect) => Effect.flatMap(jsonResponse)(fetchRequestEffect)
);
const main = fetchRequest.pipe(
  // 👇 Function composition
  Effect.flatMap(jsonResponse)
);

Wrapping values in Effect: succeed

When we wrap any API inside an Effect the goal is to compose other Effect and only at the end leave the "Effect world" and run the program.

Effect therefore offers some functions to wrap values inside Effect. Effect.succeed does just that:

const num: number = 10;
const numEffect: Effect<number> = Effect.succeed(10);