Effect-TS patterns for Error Management. Use when working with error management in Effect-TS applications.
This skill provides 15 curated Effect-TS patterns for error management. Use this skill when working on tasks related to:
Rule: Use Option.match() and Either.match() for declarative pattern matching on optional and error-prone values
Good Example:
import { Option } from "effect";
const getUserName = (id: number): Option.Option<string> => {
return id === 1 ? Option.some("Alice") : Option.none();
};
// Using .match() for declarative pattern matching
const displayUser = (id: number): string =>
getUserName(id).pipe(
Option.match({
onNone: () => "Guest User",
onSome: (name) => `Hello, ${name}!`,
})
);
console.log(displayUser(1)); // "Hello, Alice!"
console.log(displayUser(999)); // "Guest User"
import { Either } from "effect";
const validateAge = (age: number): Either.Either<number, string> => {
return age >= 18
? Either.right(age)
: Either.left("Must be 18 or older");
};
// Using .match() for error handling
const processAge = (age: number): string =>
validateAge(age).pipe(
Either.match({
onLeft: (error) => `Validation failed: ${error}`,
onRight: (validAge) => `Age ${validAge} is valid`,
})
);
console.log(processAge(25)); // "Age 25 is valid"
console.log(processAge(15)); // "Validation failed: Must be 18 or older"
When dealing with nested Option and Either, use nested .match() calls:
import { Option, Either } from "effect";
interface UserProfile {
name: string;
age: number;
}
const getUserProfile = (
id: number
): Option.Option<Either.Either<string, UserProfile>> => {
if (id === 0) return Option.none(); // User not found
if (id === 1) return Option.some(Either.left("Profile incomplete"));
return Option.some(Either.right({ name: "Bob", age: 25 }));
};
// Nested matching - first on Option, then on Either
const displayProfile = (id: number): string =>
getUserProfile(id).pipe(
Option.match({
onNone: () => "User not found",
onSome: (result) =>
result.pipe(
Either.match({
onLeft: (error) => `Error: ${error}`,
onRight: (profile) => `${profile.name} (${profile.age})`,
})
),
})
);
console.log(displayProfile(0)); // "User not found"
console.log(displayProfile(1)); // "Error: Profile incomplete"
console.log(displayProfile(2)); // "Bob (25)"
Anti-Pattern:
Avoid manual conditional checks and nested ternaries:
// ❌ ANTI-PATTERN: Imperative checks with isSome/isLeft
const name = getUserName(1);
let result: string;
if (Option.isSome(name)) {
result = `Hello, ${name.value}!`;
} else {
result = "Guest User";
}
// ❌ ANTI-PATTERN: Nested ternaries
const ageResult = validateAge(25);
const message = ageResult.pipe(
Either.match({
onLeft: () => "Invalid",
onRight: (age) => age >= 21 ? "Can drink" : "Cannot drink",
})
);
// ❌ ANTI-PATTERN: Chained if-else instead of match
function processValue(value: Option.Option<number>): string {
if (Option.isSome(value)) {
if (value.value > 0) {
return "Positive";
} else if (value.value < 0) {
return "Negative";
} else {
return "Zero";
}
}
return "No value";
}
Why these are worse:
Rationale:
When you need to handle Option or Either values, use the .match() combinator instead of imperative checks. The .match() method provides a declarative, exhaustive way to handle all cases (Some/None for Option, Right/Left for Either) in a single expression.
Use .match() when:
The .match() combinator is superior to manual checks (isSome(), isLeft()) because:
.pipe() for chaining operationsWithout .match(), you'd need imperative conditionals, which are harder to read and easier to get wrong.
Rule: Use catchAll or catchTag to recover from errors and keep your program running.
Good Example:
import { Effect, Data } from "effect"
// ============================================
// 1. Define typed errors
// ============================================
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly url: string
}> {}
class NotFoundError extends Data.TaggedError("NotFoundError")<{
readonly resource: string
}> {}
// ============================================
// 2. Functions that can fail
// ============================================
const fetchData = (url: string): Effect.Effect<string, NetworkError> =>
url.startsWith("http")
? Effect.succeed(`Data from ${url}`)
: Effect.fail(new NetworkError({ url }))
const findUser = (id: string): Effect.Effect<{ id: string; name: string }, NotFoundError> =>
id === "123"
? Effect.succeed({ id, name: "Alice" })
: Effect.fail(new NotFoundError({ resource: `user:${id}` }))
// ============================================
// 3. Handle ALL errors with catchAll
// ============================================
const withFallback = fetchData("invalid-url").pipe(
Effect.catchAll((error) => {
console.log(`Failed: ${error.url}, using fallback`)
return Effect.succeed("Fallback data")
})
)
// Result: "Fallback data"
// ============================================
// 4. Handle SPECIFIC errors with catchTag
// ============================================
const findUserOrDefault = (id: string) =>
findUser(id).pipe(
Effect.catchTag("NotFoundError", (error) => {
console.log(`User not found: ${error.resource}`)
return Effect.succeed({ id: "guest", name: "Guest User" })
})
)
// ============================================
// 5. Handle MULTIPLE error types
// ============================================
const fetchUser = (url: string, id: string) =>
Effect.gen(function* () {
yield* fetchData(url)
return yield* findUser(id)
})
const robustFetchUser = (url: string, id: string) =>
fetchUser(url, id).pipe(
Effect.catchTags({
NetworkError: (e) => Effect.succeed({ id: "offline", name: `Offline (${e.url})` }),
NotFoundError: (e) => Effect.succeed({ id: "unknown", name: `Unknown (${e.resource})` }),
})
)
// ============================================
// 6. Run the examples
// ============================================
const program = Effect.gen(function* () {
// catchAll example
const data = yield* withFallback
yield* Effect.log(`Got data: ${data}`)
// catchTag example
const user = yield* findUserOrDefault("999")
yield* Effect.log(`Got user: ${user.name}`)
// Multiple error types
const result = yield* robustFetchUser("invalid", "999")
yield* Effect.log(`Robust result: ${result.name}`)
})
Effect.runPromise(program)
Rationale:
Handle errors in Effect using catchAll to catch any error, or catchTag to handle specific error types.
Effect makes errors explicit in your types:
Rule: Use match to pattern match on the result of an Effect, Option, or Either, handling both success and failure cases declaratively.
Good Example:
import { Effect, Option, Either } from "effect";
// Effect: Handle both success and failure
const effect = Effect.fail("Oops!").pipe(
Effect.match({
onFailure: (err) => `Error: ${err}`,
onSuccess: (value) => `Success: ${value}`,
})
); // Effect<string>
// Option: Handle Some and None cases
const option = Option.some(42).pipe(
Option.match({
onNone: () => "No value",
onSome: (n) => `Value: ${n}`,
})
); // string
// Either: Handle Left and Right cases
const either = Either.left("fail").pipe(
Either.match({
onLeft: (err) => `Error: ${err}`,
onRight: (value) => `Value: ${value}`,
})
); // string
Explanation:
Effect.match lets you handle both the error and success channels in one place.Option.match and Either.match let you handle all possible cases for these types, making your code exhaustive and safe.Anti-Pattern:
Using nested if/else or switch statements to check for success/failure, or ignoring possible error/none/left cases, which leads to brittle and less readable code.
Rationale:
Use the match combinator to handle both success and failure cases in a single, declarative place.
This works for Effect, Option, and Either, and is the foundation for robust, readable error handling and branching.
Pattern matching with match keeps your code clear and type-safe, ensuring you handle all possible outcomes.
It avoids scattered if/else or switch statements and makes your intent explicit.
Rule: Use isSome, isNone, isLeft, and isRight to check Option and Either cases for simple, type-safe conditional logic.
Good Example:
import { Option, Either } from "effect";
// Option: Check if value is Some or None
const option = Option.some(42);
if (Option.isSome(option)) {
// option.value is available here
console.log("We have a value:", option.value);
} else if (Option.isNone(option)) {
console.log("No value present");
}
// Either: Check if value is Right or Left
const either = Either.left("error");
if (Either.isRight(either)) {
// either.right is available here
console.log("Success:", either.right);
} else if (Either.isLeft(either)) {
// either.left is available here
console.log("Failure:", either.left);
}
// Filtering a collection of Options
const options = [Option.some(1), Option.none(), Option.some(3)];
const presentValues = options.filter(Option.isSome).map((o) => o.value); // [1, 3]
Explanation:
Option.isSome and Option.isNone let you check for presence or absence.Either.isRight and Either.isLeft let you check for success or failure.Anti-Pattern:
Manually checking internal tags or properties (e.g., option._tag === "Some"), or using unsafe type assertions, which is less safe and less readable than using the provided predicates.
Rationale:
Use the isSome, isNone, isLeft, and isRight predicates to check the case of an Option or Either for simple, type-safe branching.
These are useful when you need to perform quick checks or filter collections based on presence or success.
These predicates provide a concise, type-safe way to check which case you have, without resorting to manual property checks or unsafe type assertions.
Rule: Handle errors with catchTag, catchTags, and catchAll.
Good Example:
import { Data, Effect } from "effect";
// Define domain types
interface User {
readonly id: string;
readonly name: string;
}
// Define specific error types
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly url: string;
readonly code: number;
}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string;
readonly message: string;
}> {}
class NotFoundError extends Data.TaggedError("NotFoundError")<{
readonly id: string;
}> {}
// Define UserService
class UserService extends Effect.Service<UserService>()("UserService", {
sync: () => ({
// Fetch user data
fetchUser: (
id: string
): Effect.Effect<User, NetworkError | NotFoundError> =>
Effect.gen(function* () {
yield* Effect.logInfo(`Fetching user with id: ${id}`);
if (id === "invalid") {
const url = "/api/users/" + id;
yield* Effect.logWarning(`Network error accessing: ${url}`);
return yield* Effect.fail(new NetworkError({ url, code: 500 }));
}
if (id === "missing") {
yield* Effect.logWarning(`User not found: ${id}`);
return yield* Effect.fail(new NotFoundError({ id }));
}
const user = { id, name: "John Doe" };
yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);
return user;
}),
// Validate user data
validateUser: (user: User): Effect.Effect<string, ValidationError> =>
Effect.gen(function* () {
yield* Effect.logInfo(`Validating user: ${JSON.stringify(user)}`);
if (user.name.length < 3) {
yield* Effect.logWarning(
`Validation failed: name too short for user ${user.id}`
);
return yield* Effect.fail(
new ValidationError({ field: "name", message: "Name too short" })
);
}
const message = `User ${user.name} is valid`;
yield* Effect.logInfo(message);
return message;
}),
}),
}) {}
// Compose operations with error handling using catchTags
const processUser = (
userId: string
): Effect.Effect<string, never, UserService> =>
Effect.gen(function* () {
const userService = yield* UserService;
yield* Effect.logInfo(`=== Processing user ID: ${userId} ===`);
const result = yield* userService.fetchUser(userId).pipe(
Effect.flatMap(userService.validateUser),
// Handle different error types with specific recovery logic
Effect.catchTags({
NetworkError: (e) =>
Effect.gen(function* () {
const message = `Network error: ${e.code} for ${e.url}`;
yield* Effect.logError(message);
return message;
}),
NotFoundError: (e) =>
Effect.gen(function* () {
const message = `User ${e.id} not found`;
yield* Effect.logWarning(message);
return message;
}),
ValidationError: (e) =>
Effect.gen(function* () {
const message = `Invalid ${e.field}: ${e.message}`;
yield* Effect.logWarning(message);
return message;
}),
})
);
yield* Effect.logInfo(`Result: ${result}`);
return result;
});
// Test with different scenarios
const runTests = Effect.gen(function* () {
yield* Effect.logInfo("=== Starting User Processing Tests ===");
const testCases = ["valid", "invalid", "missing"];
const results = yield* Effect.forEach(testCases, (id) => processUser(id));
yield* Effect.logInfo("=== User Processing Tests Complete ===");
return results;
});
// Run the program
Effect.runPromise(Effect.provide(runTests, UserService.Default));
Explanation:
Use catchTag to handle specific error types in a type-safe, composable way.
Anti-Pattern:
Using try/catch blocks inside your Effect compositions. It breaks the
declarative flow and bypasses Effect's powerful, type-safe error channels.
Rationale:
To recover from failures, use the catch* family of functions.
Effect.catchTag for specific tagged errors, Effect.catchTags for multiple,
and Effect.catchAll for any error.
Effect's structured error handling allows you to build resilient applications.
By using tagged errors and catchTag, you can handle different failure
scenarios with different logic in a type-safe way.
Rule: Use Effect.mapError to transform errors and create clean architectural boundaries between layers.
Good Example:
A UserRepository uses a Database service. The Database can fail with specific errors, but the UserRepository maps them to a single, generic RepositoryError before they are exposed to the rest of the application.
import { Effect, Data } from "effect";
// Low-level, specific errors from the database layer
class ConnectionError extends Data.TaggedError("ConnectionError") {}
class QueryError extends Data.TaggedError("QueryError") {}
// A generic error for the repository layer
class RepositoryError extends Data.TaggedError("RepositoryError")<{
readonly cause: unknown;
}> {}
// The inner service
const dbQuery = (): Effect.Effect<
{ name: string },
ConnectionError | QueryError
> => Effect.fail(new ConnectionError());
// The outer service uses `mapError` to create a clean boundary.
// Its public signature only exposes `RepositoryError`.
const findUser = (): Effect.Effect<{ name: string }, RepositoryError> =>
dbQuery().pipe(
Effect.mapError((error) => new RepositoryError({ cause: error }))
);
// Demonstrate the error mapping
const program = Effect.gen(function* () {
yield* Effect.logInfo("Attempting to find user...");
try {
const user = yield* findUser();
yield* Effect.logInfo(`Found user: ${user.name}`);
} catch (error) {
yield* Effect.logInfo("This won't be reached due to Effect error handling");
}
}).pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
if (error instanceof RepositoryError) {
yield* Effect.logInfo(`Repository error occurred: ${error._tag}`);
if (
error.cause instanceof ConnectionError ||
error.cause instanceof QueryError
) {
yield* Effect.logInfo(`Original cause: ${error.cause._tag}`);
}
} else {
yield* Effect.logInfo(`Unexpected error: ${error}`);
}
})
)
);
Effect.runPromise(program);
Anti-Pattern:
Allowing low-level, implementation-specific errors to "leak" out of a service's public API. This creates tight coupling between layers.
import { Effect } from "effect";
import { ConnectionError, QueryError } from "./somewhere"; // From previous example
// ❌ WRONG: This function's error channel is "leaky".
// It exposes the internal implementation details of the database.
const findUserUnsafely = (): Effect.Effect<
{ name: string },
ConnectionError | QueryError // <-- Leaky abstraction
> => {
// ... logic that calls the database
return Effect.fail(new ConnectionError());
};
// Now, any code that calls `findUserUnsafely` has to know about and handle
// both `ConnectionError` and `QueryError`. If we change the database,
// all of that calling code might have to change too.
Rationale:
When an inner service can fail with specific errors, use Effect.mapError in the outer service to catch those specific errors and transform them into a more general error suitable for its own domain.
This pattern is essential for creating clean architectural boundaries and preventing "leaky abstractions." An outer layer of your application (e.g., a UserService) should not expose the internal failure details of the layers it depends on (e.g., a Database that can fail with ConnectionError or QueryError).
By using Effect.mapError, the outer layer can define its own, more abstract error type (like RepositoryError) and map all the specific, low-level errors into it. This decouples the layers. If you later swap your database implementation, you only need to update the mapping logic within the repository layer; none of the code that uses the repository needs to change.
Rule: Use Schedule to create composable policies for controlling the repetition and retrying of effects.
Good Example:
This example demonstrates composition by creating a common, robust retry policy: exponential backoff with jitter, limited to 5 attempts.
import { Effect, Schedule, Duration } from "effect";
// A simple effect that can fail
const flakyEffect = Effect.try({
try: () => {
if (Math.random() > 0.2) {
throw new Error("Transient error");
}
return "Operation succeeded!";
},
catch: (error: unknown) => {
Effect.logInfo("Operation failed, retrying...");
return error;
},
});
// --- Building a Composable Schedule ---
// 1. Start with a base exponential backoff (100ms, 200ms, 400ms...)
const exponentialBackoff = Schedule.exponential("100 millis");
// 2. Add random jitter to avoid thundering herd problems
const withJitter = Schedule.jittered(exponentialBackoff);
// 3. Limit the schedule to a maximum of 5 repetitions
const limitedWithJitter = Schedule.compose(withJitter, Schedule.recurs(5));
// --- Using the Schedule ---
const program = Effect.gen(function* () {
yield* Effect.logInfo("Starting operation...");
const result = yield* Effect.retry(flakyEffect, limitedWithJitter);
yield* Effect.logInfo(`Final result: ${result}`);
});
// Run the program
Effect.runPromise(program);
Anti-Pattern:
Writing manual, imperative retry logic. This is verbose, stateful, hard to reason about, and not easily composable.
import { Effect } from "effect";
import { flakyEffect } from "./somewhere";
// ❌ WRONG: Manual, stateful, and complex retry logic.
function manualRetry(
effect: typeof flakyEffect,
retriesLeft: number,
delay: number
): Effect.Effect<string, "ApiError"> {
return effect.pipe(
Effect.catchTag("ApiError", () => {
if (retriesLeft > 0) {
return Effect.sleep(delay).pipe(
Effect.flatMap(() => manualRetry(effect, retriesLeft - 1, delay * 2))
);
}
return Effect.fail("ApiError" as const);
})
);
}
const program = manualRetry(flakyEffect, 5, 100);
Rationale:
A Schedule<In, Out> is a highly-composable blueprint that defines a recurring schedule. It takes an input of type In (e.g., the error from a failed effect) and produces an output of type Out (e.g., the decision to continue). Use Schedule with operators like Effect.repeat and Effect.retry to control complex repeating logic.
While you could write manual loops or recursive functions, Schedule provides a much more powerful, declarative, and composable way to manage repetition. The key benefits are:
Schedule.recurs, Schedule.exponential, and Schedule.jittered.Schedule keeps track of its own state (like the number of repetitions), making it easy to create policies that depend on the execution history.Rule: Leverage Effect's built-in structured logging.
Good Example:
import { Effect } from "effect";
const program = Effect.logDebug("Processing user", { userId: 123 });
// Run the program with debug logging enabled
Effect.runSync(
program.pipe(Effect.tap(() => Effect.log("Debug logging enabled")))
);
Explanation:
Using Effect's logging system ensures your logs are
npx skills add PaulJPhilp/effect-patterns-error-management下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer