Layer.fail

A Layer represents a constructor. That construction logic is effectful. In particular, it can go wrong.

For example, suppose your application uses a DatabaseService. What should happen if your application is unable to establish a connection to that database? What should happen if your application is unable to locate the credentials to connect? These are two very different errors. Using Effect we can model these different failure modes.

The simplest way to represent a failure constructor is with Layer.fail.

import { Data, Effect, Layer } from "effect";
import { RandomService } from "./lib";

class MissingEnvironmentVariable extends Data.TaggedError(
  "@fred.c/MissingEnvironmentVariable"
)<{
  variable: string;
}> {}

//      ┌─── Layer.Layer<unknown, MissingEnvironmentVariable, never>
//      ▼
const badLayer = Layer.fail(
  new MissingEnvironmentVariable({
    variable: "RANDOM_SEED",
  })
);

Notice that Layer.succeed (26) and Layer.fail (30) mirror the equivalent constructors Effect.succeed (1) and Effect.fail (5) from the Effect module. You will find this kind of symmetry in many parts of the Effect ecosystem.

Much like Effect, a Layer has three type parameters, and the second type parameter represents the ways in which this effectful computation could go wrong.

Let’s see what happens when we use this layer in a simple app.

const app = RandomService.pipe(
  Effect.flatMap((random) => random.next),
  Effect.map((value) => console.log(`random value: ${value}`)),
  //               ┌─── never
  //               ▼
  Effect.catchAll((e) => {
    console.error(`${e} should never occur`);
    return Effect.succeed(undefined);
  })
);

const main = app.pipe(
  Effect.provide(badLayer),
  //               ┌─── MissingEnvironmentVariable
  //               ▼
  Effect.catchAll((e) => {
    console.error(`You must set the environment variable ${e.variable}`);
    return Effect.succeed(undefined);
  })
);

Effect.runSync(main);

Inside of our core app itself there are no failures. RandomService knows nothing about any failures. It is only once we provide the layer that failure is possible. We can write error handling logic to deal with that particular failure in a type-safe way.

Note also that the error will only be logged when the Effect is run. Effect.provide does not “run” the Layer. Instead, much like Effect.flatMap, it represents an instruction that says the Layer should be run.

We could have different error handling strategies depending on how we want our app to behave. In this case we choose to display an error message, but we could also have decided to provide a fallback implementation of RandomService instead. What about the DatabaseService example? Perhaps if credentials were not found in the environment variable then the app should look in a config file, or exit immediately. If the app cannot connect to the database, perhaps it should retry up to 5 times with an exponential backoff.

In simple programs we will tend to provide all layers at the very edge of the program, but in more complex applications the layers may be provided regionally. For example, in a web service there might be a different layer for each API endpoint - providing only the services that the endpoint needs. This can give you a type-safe guarantee that only the admin endpoints use AdminService.

Let’s recap:

  • a Layer conceptually represents an effectful constructor
  • these effectful constructors can fail at runtime
  • The second parameter represents failures that can occur when this Layer is run
  • providing a Layer will expose those errors in the Effect’s error channel
  • we can handle Layer-related failures using the same error-handling helpers from the Effect module