tryPromise: Error handling

The first benefit of effect is error handling.

However, Effect.promise does not handle errors.

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

Effect.runPromise(main);

When you use Effect.promise effect assumes that the operation can never fail. In practice main works just like Promise<unknown> but wrapped in Effect.

If the function inside Effect.promise throws, then executing the effect will throw as well! 👇

> effect-getting-started-course@1.0.0 dev
> tsx src/index.ts

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

SyntaxError: Unexpected token 'N', "Not Found" is not valid JSON

Since we know that fetch can fail we need another function: Effect.tryPromise.

import { Effect } from "effect";

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

Whereas Effect.promise returned an Effect<Response>, Effect.tryPromise instead collects any error and returns Effect<Response, UnknownException>.

Types for errors

What is UnknownException? Why Effect has 2 generic parameters now?

When you execute any plain typescript function you have no way of knowing what may go wrong unless you read the function implementation:

const request: Promise<Response> = fetch("https://pokeapi.co/api/v2/pokemon/garchomp/");

Simple example: how do you know if fetch can fail? request is Promise<Response>, so no idea if something can go wrong, or how.

This problem becomes even worst when you compose multiple functions.

const main: Promise<unknown> =
  fetch("https://pokeapi.co/api/v2/pokemon/garchomp/")
    .then((response) => response.json());

Since both request and json may fail you are left with two choices:

  1. Read the implementation of all the functions and check for any throw, then wrap each function with try/catch
const main = async (): Promise<unknown> => {
  let response;
  try {
    response = await fetch("https://pokeapi.co/api/v2/pokemon/garchomp/");
  } catch (e) {
    // Error with fetch: Do something here
    return;
  }

  try {
    return response.json();
  } catch (e) {
    // Error with json: Do something here
    return;
  }
};
  1. Wrap everything in a single try/catch and report a generic "Some error happened"
const main = async (): Promise<unknown> => {
  try {
    const response = await fetch("https://pokeapi.co/api/v2/pokemon/garchomp/");
    return response.json();
  } catch (e) {
    // Some error somewhere 💁🏼‍♂️
    return;
  }
};

Effect solves this problem by providing the error directly in the type:

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

When we compose fetchRequest and jsonResponse we get a program main that returns Effect<unknown, UnknownException>:

  • unknown is the return type of calling response.json() when everything works as expected
  • UnknownException is the error type when either fetchRequest or jsonResponse fails
/// Effect<Response, UnknownException>
const fetchRequest = Effect.tryPromise(
  () => fetch("https://pokeapi.co/api/v2/pokemon/garchomp/")
);

/// Effect<unknown, UnknownException>
const jsonResponse = (response: Response) => Effect.tryPromise(
  () => response.json()
);

/// Effect<unknown, UnknownException>
const main = Effect.flatMap(fetchRequest, jsonResponse);
Effect Playground

We still cannot distinguish between errors in fetchRequest and jsonResponse since both are typed as UnknownException.

Don't worry! tryPromise also allows to define custom errors, we are going to learn how in the following lessons.