| 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 |
|
|||||
| rule |
|
|||||
| related |
|
|||||
| author | effect_website | |||||
| lessonOrder | 1 |
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).
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.
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);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);
});