Skip to content

Latest commit

 

History

History
151 lines (125 loc) · 4.67 KB

File metadata and controls

151 lines (125 loc) · 4.67 KB
title Accumulate Multiple Errors with Either
id accumulate-multiple-errors-with-either
skillLevel intermediate
applicationPatternId domain-modeling
summary Use Either<E, A> to represent computations that can fail, allowing you to accumulate multiple errors instead of short-circuiting on the first one.
tags
either
validation
error-accumulation
schema
data
rule
description
Use Either to accumulate multiple validation errors instead of failing on the first one.
related
define-contracts-with-schema
distinguish-not-found-from-errors
author effect_website
lessonOrder 1

Guideline

When you need to perform multiple validation checks and collect all failures, use the Either<E, A> data type. Either represents a value that can be one of two possibilities: a Left<E> (typically for failure) or a Right<A> (typically for success).


Rationale

The Effect error channel is designed to short-circuit. The moment an Effect fails, the entire computation stops and the error is propagated. This is perfect for handling unrecoverable errors like a lost database connection.

However, for tasks like validating a user's input, this is poor user experience. You want to show the user all of their mistakes at once.

Either is the solution. Since it's a pure data structure, you can run multiple checks that each return an Either, and then combine the results to accumulate all the Left (error) values. The Effect/Schema module uses this pattern internally to provide powerful error accumulation.


Good Example

Using Schema.decode with the allErrors: true option demonstrates this pattern perfectly. The underlying mechanism uses Either to collect all parsing errors into an array instead of stopping at the first one.

import { Effect, Schema, Data, Either } from "effect";

// Define validation error type
class ValidationError extends Data.TaggedError("ValidationError")<{
  readonly field: string;
  readonly message: string;
}> {}

// Define user type
type User = {
  name: string;
  email: string;
};

// Define schema with custom validation
const UserSchema = Schema.Struct({
  name: Schema.String.pipe(
    Schema.minLength(3),
    Schema.filter((name) => /^[A-Za-z\s]+$/.test(name), {
      message: () => "name must contain only letters and spaces",
    })
  ),
  email: Schema.String.pipe(
    Schema.pattern(/@/),
    Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, {
      message: () => "email must be a valid email address",
    })
  ),
});

// Example inputs
const invalidInputs: User[] = [
  {
    name: "Al", // Too short
    email: "bob-no-at-sign.com", // Invalid pattern
  },
  {
    name: "John123", // Contains numbers
    email: "john@incomplete", // Invalid email
  },
  {
    name: "Alice Smith", // Valid
    email: "alice@example.com", // Valid
  },
];

// Validate a single user
const validateUser = (input: User) =>
  Effect.gen(function* () {
    const result = yield* Schema.decode(UserSchema)(input, { errors: "all" });
    return result;
  });

// Process multiple users and accumulate all errors
const program = Effect.gen(function* () {
  yield* Effect.log("Validating users...\n");

  for (const input of invalidInputs) {
    const result = yield* Effect.either(validateUser(input));

    yield* Effect.log(`Validating user: ${input.name} <${input.email}>`);

    // Handle success and failure cases separately for clarity
    // Using Either.match which is the idiomatic way to handle Either values
    yield* Either.match(result, {
      onLeft: (error) =>
        Effect.gen(function* () {
          yield* Effect.log("❌ Validation failed:");
          yield* Effect.log(error.message);
          yield* Effect.log(""); // Empty line for readability
        }),
      onRight: (user) =>
        Effect.gen(function* () {
          yield* Effect.log(`✅ User is valid: ${JSON.stringify(user)}`);
          yield* Effect.log(""); // Empty line for readability
        }),
    });
  }
});

// Run the program
Effect.runSync(program);

Anti-Pattern

Using Effect's error channel for validation that requires multiple error messages. The code below will only ever report the first error it finds, because Effect.fail short-circuits the entire Effect.gen block.

import { Effect } from "effect";

const validateWithEffect = (input: { name: string; email: string }) =>
  Effect.gen(function* () {
    if (input.name.length < 3) {
      // The program will fail here and never check the email.
      return yield* Effect.fail("Name is too short.");
    }
    if (!input.email.includes("@")) {
      return yield* Effect.fail("Email is invalid.");
    }
    return yield* Effect.succeed(input);
  });