Providing services to run effects

Similar to errors, we can remove the dependency from the type by providing a concrete implementation before running the effect.

We reached the point in which we actually need to create a concrete implementation of the service. A service created using Context has a .of method that allows to create an instance of the service.

Remember the getPokemon function we refactored before? Let's use it to create the PokeApi service:

PokeApi.ts
export const PokeApi = Context.GenericTag<PokeApi>("PokeApi");

// 👉 `PokeApi.of` defines a concrete implementation for the service
export const PokeApiLive = PokeApi.of({
  getPokemon: Effect.gen(function* () {
    const baseUrl = yield* Config.string("BASE_URL");

    const response = yield* Effect.tryPromise({
      try: () => fetch(`${baseUrl}/api/v2/pokemon/garchomp/`),
      catch: () => new FetchError(),
    });

    if (!response.ok) {
      return yield* new FetchError();
    }

    const json = yield* Effect.tryPromise({
      try: () => response.json(),
      catch: () => new JsonError(),
    });

    return yield* Schema.decodeUnknown(Pokemon)(json);
  }),
});

We can create as many concrete implementations as we want from the same abstract service by using .of.

By convention the production implementation has a -Live suffix. You can then create a -Test/-Mock/-Dev implementation if needed.

export const PokeApi = Context.GenericTag<PokeApi>("PokeApi");> 

export const PokeApiTest = PokeApi.of({
  getPokemon: Effect.succeed({
    id: 1,
    height: 10,
    weight: 10,
    order: 1,
    name: "myname",
  }),
});

Now we need to provide PokeApiLive to program to run the app. We use Effect.provideService to do this:

  • First parameter is the service Context definition
  • Second parameter is the concrete implementation of the service
index.ts
import { Effect } from "effect";
import { PokeApi, PokeApiLive } from "./PokeApi";

const program = Effect.gen(function* () {
  const pokeApi = yield* PokeApi;
  return yield* pokeApi.getPokemon;
});

const runnable = program.pipe(Effect.provideService(PokeApi, PokeApiLive));

The type of runnable is now Effect<Pokemon, FetchError | JsonError | ParseError | ConfigError, never>: we provided the dependency on PokeApi, removed it from the type, and now the third parameter is never.

We can now compose the full app and run it without errors:

index.ts
import { Effect } from "effect";
import { PokeApi, PokeApiLive } from "./PokeApi";

const program = Effect.gen(function* () {
  const pokeApi = yield* PokeApi;
  return yield* pokeApi.getPokemon;
});

const runnable = program.pipe(Effect.provideService(PokeApi, PokeApiLive));

const main = runnable.pipe(
  Effect.catchTags({
    FetchError: () => Effect.succeed("Fetch error"),
    JsonError: () => Effect.succeed("Json error"),
    ParseError: () => Effect.succeed("Parse error"),
  })
);

Effect.runPromise(main).then(console.log);
  • program: Full Effect implementation with errors and dependencies included in the type
  • runnable: Provide all the dependencies to program to make the third type parameter never
  • main: Handle all (or part of) the errors from runnable to make the second type parameter never