Effect Patterns Rules
Generated by ep on 2026-02-06T22:56:11.471Z. Source: content/published/rules
Checking Option and Either Cases
- ID: checking-option-and-either-cases
- Skill Level: general
- Use Cases: error-management
Use isSome, isNone, isLeft, and isRight to check Option and Either cases for simple, type-safe conditional logic.
description: Use isSome, isNone, isLeft, and isRight to check Option and Either cases for simple, type-safe conditional logic. globs: "**/*.ts" alwaysApply: true
Checking Option and Either Cases
Rule: Use isSome, isNone, isLeft, and isRight to check Option and Either cases for simple, type-safe conditional logic.
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.isSomeandOption.isNonelet you check for presence or absence.Either.isRightandEither.isLeftlet you check for success or failure.- These are especially useful for filtering or quick conditional logic.
Explanation:
These predicates provide a concise, type-safe way to check which case you have, without resorting to manual property checks or unsafe type assertions.
Conditionally Branching Workflows
- ID: conditionally-branching-workflows
- Skill Level: intermediate
- Use Cases: error-management
Use predicate-based operators like Effect.filter and Effect.if to declaratively control workflow branching.
description: Use predicate-based operators like Effect.filter and Effect.if to declaratively control workflow branching. globs: "**/*.ts" alwaysApply: true
Conditionally Branching Workflows
Rule: Use predicate-based operators like Effect.filter and Effect.if to declaratively control workflow branching.
Example
Here, we use Effect.filterOrFail with named predicates to validate a user before proceeding. The intent is crystal clear, and the business rules (isActive, isAdmin) are reusable.
import { Effect } from "effect";
interface User {
id: number;
status: "active" | "inactive";
roles: string[];
}
type UserError = "DbError" | "UserIsInactive" | "UserIsNotAdmin";
const findUser = (id: number): Effect.Effect<User, "DbError"> =>
Effect.succeed({ id, status: "active", roles: ["admin"] });
// Reusable, testable predicates that document business rules.
const isActive = (user: User): boolean => user.status === "active";
const isAdmin = (user: User): boolean => user.roles.includes("admin");
const program = (id: number): Effect.Effect<string, UserError> =>
findUser(id).pipe(
// Validate user is active using Effect.filterOrFail
Effect.filterOrFail(isActive, () => "UserIsInactive" as const),
// Validate user is admin using Effect.filterOrFail
Effect.filterOrFail(isAdmin, () => "UserIsNotAdmin" as const),
// Success case
Effect.map((user) => `Welcome, admin user #${user.id}!`)
);
// We can then handle the specific failures in a type-safe way.
const handled = program(123).pipe(
Effect.match({
onFailure: (error) => {
switch (error) {
case "UserIsNotAdmin":
return "Access denied: requires admin role.";
case "UserIsInactive":
return "Access denied: user is not active.";
case "DbError":
return "Error: could not find user.";
default:
return `Unknown error: ${error}`;
}
},
onSuccess: (result) => result,
})
);
// Run the program
const programWithLogging = Effect.gen(function* () {
const result = yield* handled;
yield* Effect.log(result);
return result;
});
Effect.runPromise(programWithLogging);
Explanation:
This pattern allows you to embed decision-making logic directly into your composition pipelines, making your code more declarative and readable. It solves two key problems:
- Separation of Concerns: It cleanly separates the logic of producing a value from the logic of validating or making decisions about that value.
- Reusable Business Logic: A predicate function (e.g.,
const isAdmin = (user: User) => ...) becomes a named, reusable, and testable piece of business logic, far superior to scattering inlineifstatements throughout your code.
Using these operators turns conditional logic into a composable part of your Effect, rather than an imperative statement that breaks the flow.
Control Repetition with Schedule
- ID: control-repetition-with-schedule
- Skill Level: intermediate
- Use Cases: error-management
Use Schedule to create composable policies for controlling the repetition and retrying of effects.
description: Use Schedule to create composable policies for controlling the repetition and retrying of effects. globs: "**/*.ts" alwaysApply: true
Control Repetition with Schedule
Rule: Use Schedule to create composable policies for controlling the repetition and retrying of effects.
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);
Explanation:
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:
- Declarative: You separate the what (the effect to run) from the how and when (the schedule it runs on).
- Composable: You can build complex schedules from simple, primitive ones. For example, you can create a schedule that runs "up to 5 times, with an exponential backoff, plus some random jitter" by composing
Schedule.recurs,Schedule.exponential, andSchedule.jittered. - Stateful: A
Schedulekeeps track of its own state (like the number of repetitions), making it easy to create policies that depend on the execution history.
Effectful Pattern Matching with matchEffect
- ID: effectful-pattern-matching-with-matcheffect
- Skill Level: general
- Use Cases: error-management
Use matchEffect to pattern match on the result of an Effect, running effectful logic for both success and failure cases.
description: Use matchEffect to pattern match on the result of an Effect, running effectful logic for both success and failure cases. globs: "**/*.ts" alwaysApply: true
Effectful Pattern Matching with matchEffect
Rule: Use matchEffect to pattern match on the result of an Effect, running effectful logic for both success and failure cases.
Example
import { Effect } from "effect";
// Effect: Run different Effects on success or failure
const effect = Effect.fail("Oops!").pipe(
Effect.matchEffect({
onFailure: (err) => Effect.logError(`Error: ${err}`),
onSuccess: (value) => Effect.log(`Success: ${value}`),
})
); // Effect<void>
Explanation:
matchEffectallows you to run an Effect for both the success and failure cases.- This is useful for logging, cleanup, retries, or any effectful side effect that depends on the outcome.
Explanation:
Sometimes, handling a success or failure requires running additional Effects (e.g., logging, retries, cleanup).
matchEffect lets you do this declaratively, keeping your code composable and type-safe.
Handle Errors with catchTag, catchTags, and catchAll
- ID: handle-errors-with-catchtag-catchtags-and-catchall
- Skill Level: intermediate
- Use Cases: error-management
Handle errors with catchTag, catchTags, and catchAll.
description: Handle errors with catchTag, catchTags, and catchAll. globs: "**/*.ts" alwaysApply: true
Handle Errors with catchTag, catchTags, and catchAll
Rule: Handle errors with catchTag, catchTags, and catchAll.
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.
Explanation:
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.
Handle Flaky Operations with Retries and Timeouts
- ID: handle-flaky-operations-with-retries-and-timeouts
- Skill Level: intermediate
- Use Cases: error-management
Use Effect.retry and Effect.timeout to build resilience against slow or intermittently failing effects.
description: Use Effect.retry and Effect.timeout to build resilience against slow or intermittently failing effects. globs: "**/*.ts" alwaysApply: true
Handle Flaky Operations with Retries and Timeouts
Rule: Use Effect.retry and Effect.timeout to build resilience against slow or intermittently failing effects.
Example
This program attempts to fetch data from a flaky API. It will retry the request up to 3 times with increasing delays if it fails. It will also give up entirely if any single attempt takes longer than 2 seconds.
import { Data, Duration, Effect, Schedule } from "effect";
// Define domain types
interface ApiResponse {
readonly data: string;
}
// Define error types
class ApiError extends Data.TaggedError("ApiError")<{
readonly message: string;
readonly attempt: number;
}> {}
class TimeoutError extends Data.TaggedError("TimeoutError")<{
readonly duration: string;
readonly attempt: number;
}> {}
// Define API service
class ApiService extends Effect.Service<ApiService>()("ApiService", {
sync: () => ({
// Flaky API call that might fail or be slow
fetchData: (): Effect.Effect<ApiResponse, ApiError | TimeoutError> =>
Effect.gen(function* () {
const attempt = Math.floor(Math.random() * 5) + 1;
yield* Effect.logInfo(`Attempt ${attempt}: Making API call...`);
if (Math.random() > 0.3) {
yield* Effect.logWarning(`Attempt ${attempt}: API call failed`);
return yield* Effect.fail(
new ApiError({
message: "API Error",
attempt,
})
);
}
const delay = Math.random() * 3000;
yield* Effect.logInfo(
`Attempt ${attempt}: API call will take ${delay.toFixed(0)}ms`
);
yield* Effect.sleep(Duration.millis(delay));
const response = { data: "some important data" };
yield* Effect.logInfo(
`Attempt ${attempt}: API call succeeded with data: ${JSON.stringify(response)}`
);
return response;
}),
}),
}) {}
// Define retry policy: exponential backoff, up to 3 retries
const retryPolicy = Schedule.exponential(Duration.millis(100)).pipe(
Schedule.compose(Schedule.recurs(3)),
Schedule.tapInput((error: ApiError | TimeoutError) =>
Effect.logWarning(
`Retrying after error: ${error._tag} (Attempt ${error.attempt})`
)
)
);
// Create program with proper error handling
const program = Effect.gen(function* () {
const api = yield* ApiService;
yield* Effect.logInfo("=== Starting API calls with retry and timeout ===");
// Make multiple test calls
for (let i = 1; i <= 3; i++) {
yield* Effect.logInfo(`\n--- Test Call ${i} ---`);
const result = yield* api.fetchData().pipe(
Effect.timeout(Duration.seconds(2)),
Effect.catchTag("TimeoutException", () =>
Effect.fail(new TimeoutError({ duration: "2 seconds", attempt: i }))
),
Effect.retry(retryPolicy),
Effect.catchTags({
ApiError: (error) =>
Effect.gen(function* () {
yield* Effect.logError(
`All retries failed: ${error.message} (Last attempt: ${error.attempt})`
);
return { data: "fallback data due to API error" } as ApiResponse;
}),
TimeoutError: (error) =>
Effect.gen(function* () {
yield* Effect.logError(
`All retries timed out after ${error.duration} (Last attempt: ${error.attempt})`
);
return { data: "fallback data due to timeout" } as ApiResponse;
}),
})
);
yield* Effect.logInfo(`Result: ${JSON.stringify(result)}`);
}
yield* Effect.logInfo("\n=== API calls complete ===");
});
// Run the program
Effect.runPromise(Effect.provide(program, ApiService.Default));
Explanation:
In distributed systems, failure is normal. APIs can fail intermittently, and network latency can spike. Hard-coding your application to try an operation only once makes it brittle.
-
Retries: The
Effect.retryoperator, combined with aSchedulepolicy, provides a powerful, declarative way to handle transient failures. Instead of writing complextry/catchloops, you can simply define a policy like "retry 3 times, with an exponential backoff delay between attempts." -
Timeouts: An operation might not fail, but instead hang indefinitely.
Effect.timeoutprevents this by racing your effect against a timer. If your effect doesn't complete within the specified duration, it is automatically interrupted, preventing your application from getting stuck.
Combining these two patterns is a best practice for any interaction with an external service.
Handle Unexpected Errors by Inspecting the Cause
- ID: handle-unexpected-errors-by-inspecting-the-cause
- Skill Level: advanced
- Use Cases: core-concepts, error-management
Use Cause to inspect, analyze, and handle all possible failure modes of an Effect, including expected errors, defects, and interruptions.
description: Use Cause to inspect, analyze, and handle all possible failure modes of an Effect, including expected errors, defects, and interruptions. globs: "**/*.ts" alwaysApply: true
Handle Unexpected Errors by Inspecting the Cause
Rule: Use Cause to inspect, analyze, and handle all possible failure modes of an Effect, including expected errors, defects, and interruptions.
Example
import { Cause, Effect } from "effect";
// An Effect that may fail with an error or defect
const program = Effect.try({
try: () => {
throw new Error("Unexpected failure!");
},
catch: (err) => err,
});
// Catch all causes and inspect them
const handled = program.pipe(
Effect.catchAllCause((cause) =>
Effect.sync(() => {
if (Cause.isDie(cause)) {
console.error("Defect (die):", Cause.pretty(cause));
} else if (Cause.isFailure(cause)) {
console.error("Expected error:", Cause.pretty(cause));
} else if (Cause.isInterrupted(cause)) {
console.error("Interrupted:", Cause.pretty(cause));
}
// Handle or rethrow as needed
})
)
);
Explanation:
Causedistinguishes between expected errors (fail), defects (die), and interruptions.- Use
Cause.prettyfor human-readable error traces. - Enables advanced error handling and debugging.
Explanation:
Traditional error handling often loses information about why a failure occurred.
Cause preserves the full error context, enabling advanced debugging, error reporting, and robust recovery strategies.
Handling Specific Errors with catchTag and catchTags
- ID: handling-specific-errors-with-catchtag-and-catchtags
- Skill Level: general
- Use Cases: error-management
Use catchTag and catchTags to handle specific tagged error types in the Effect failure channel, providing targeted recovery logic.
description: Use catchTag and catchTags to handle specific tagged error types in the Effect failure channel, providing targeted recovery logic. globs: "**/*.ts" alwaysApply: true
Handling Specific Errors with catchTag and catchTags
Rule: Use catchTag and catchTags to handle specific tagged error types in the Effect failure channel, providing targeted recovery logic.
Example
import { Effect, Data } from "effect";
// Define tagged error types
class NotFoundError extends Data.TaggedError("NotFoundError")<{}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
message: string;
}> {}
type MyError = NotFoundError | ValidationError;
// Effect: Handle only ValidationError, let others propagate
const effect = Effect.fail(
new ValidationError({ message: "Invalid input" }) as MyError
).pipe(
Effect.catchTag("ValidationError", (err) =>
Effect.succeed(`Recovered from validation error: ${err.message}`)
)
); // Effect<string>
// Effect: Handle multiple error tags
const effect2 = Effect.fail(new NotFoundError() as MyError).pipe(
Effect.catchTags({
NotFoundError: () => Effect.succeed("Handled not found!"),
ValidationError: (err) =>
Effect.succeed(`Handled validation: ${err.message}`),
})
); // Effect<string>
Explanation:
catchTaglets you recover from a specific tagged error type.catchTagslets you handle multiple tagged error types in one place.- Unhandled errors continue to propagate, preserving error safety.
Explanation:
Not all errors should be handled the same way.
By matching on specific error tags, you can provide targeted recovery logic for each error type, while letting unhandled errors propagate as needed.
Leverage Effect's Built-in Structured Logging
- ID: leverage-effects-built-in-structured-logging
- Skill Level: intermediate
- Use Cases: observability, error-management
Leverage Effect's built-in structured logging.
description: Leverage Effect's built-in structured logging. globs: "**/*.ts" alwaysApply: true
Leverage Effect's Built-in Structured Logging
Rule: Leverage Effect's built-in structured logging.
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 structured, filterable,
and context-aware.
Explanation:
Effect's logger is structured, context-aware (with trace IDs), configurable
via Layer, and testable. It's a first-class citizen, not an unmanaged
side-effect.
Mapping Errors to Fit Your Domain
- ID: mapping-errors-to-fit-your-domain
- Skill Level: intermediate
- Use Cases: error-management
Use Effect.mapError to transform errors and create clean architectural boundaries between layers.
description: Use Effect.mapError to transform errors and create clean architectural boundaries between layers. globs: "**/*.ts" alwaysApply: true
Mapping Errors to Fit Your Domain
Rule: Use Effect.mapError to transform errors and create clean architectural boundaries between layers.
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);
Explanation:
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.
Matching on Success and Failure with match
- ID: matching-on-success-and-failure-with-match
- Skill Level: general
- Use Cases: error-management
Use match to pattern match on the result of an Effect, Option, or Either, handling both success and failure cases declaratively.
description: Use match to pattern match on the result of an Effect, Option, or Either, handling both success and failure cases declaratively. globs: "**/*.ts" alwaysApply: true
Matching on Success and Failure with match
Rule: Use match to pattern match on the result of an Effect, Option, or Either, handling both success and failure cases declaratively.
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.matchlets you handle both the error and success channels in one place.Option.matchandEither.matchlet you handle all possible cases for these types, making your code exhaustive and safe.
Explanation:
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.
Matching Tagged Unions with matchTag and matchTags
- ID: matching-tagged-unions-with-matchtag-and-matchtags
- Skill Level: general
- Use Cases: error-management
Use matchTag and matchTags to handle specific cases of tagged unions or custom error types in a declarative, type-safe way.
description: Use matchTag and matchTags to handle specific cases of tagged unions or custom error types in a declarative, type-safe way. globs: "**/*.ts" alwaysApply: true
Matching Tagged Unions with matchTag and matchTags
Rule: Use matchTag and matchTags to handle specific cases of tagged unions or custom error types in a declarative, type-safe way.
Example
import { Data, Effect } from "effect";
// Define a tagged error type
class NotFoundError extends Data.TaggedError("NotFoundError")<{}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
message: string;
}> {}
type MyError = NotFoundError | ValidationError;
// Effect: Match on specific error tags
const effect: Effect.Effect<string, never, never> = Effect.fail(
new ValidationError({ message: "Invalid input" }) as MyError
).pipe(
Effect.catchTags({
NotFoundError: () => Effect.succeed("Not found!"),
ValidationError: (err) =>
Effect.succeed(`Validation failed: ${err.message}`),
})
); // Effect<string>
Explanation:
matchTaglets you branch on the specific tag of a tagged union or custom error type.- This is safer and more maintainable than using
instanceofor manual property checks.
Explanation:
Tagged unions (a.k.a. algebraic data types or ADTs) are a powerful way to model domain logic.
Pattern matching on tags lets you handle each case explicitly, making your code robust, maintainable, and exhaustive.
Pattern Match on Option and Either
- ID: pattern-match-on-option-and-either
- Skill Level: general
- Use Cases: error-management
Use Option.match() and Either.match() for declarative pattern matching on optional and error-prone values
description: Use Option.match() and Either.match() for declarative pattern matching on optional and error-prone values globs: "**/*.ts" alwaysApply: true
Pattern Match on Option and Either
Rule: Use Option.match() and Either.match() for declarative pattern matching on optional and error-prone values
Example
Basic Option Matching
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"
Basic Either Matching
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"
Advanced: Nested Matching
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)"
Explanation:
The .match() combinator is superior to manual checks (isSome(), isLeft()) because:
- Declarative: Expresses intent clearly - "match on these cases"
- Type-safe: TypeScript ensures all cases are handled
- Exhaustive: You can't accidentally miss a case
- Composable: Works naturally with
.pipe()for chaining operations - Readable: The structure mirrors the data type itself
Without .match(), you'd need imperative conditionals, which are harder to read and easier to get wrong.
Retry Operations Based on Specific Errors
- ID: retry-operations-based-on-specific-errors
- Skill Level: intermediate
- Use Cases: error-management
Use predicate-based retry policies to retry an operation only for specific, recoverable errors.
description: Use predicate-based retry policies to retry an operation only for specific, recoverable errors. globs: "**/*.ts" alwaysApply: true
Retry Operations Based on Specific Errors
Rule: Use predicate-based retry policies to retry an operation only for specific, recoverable errors.
Example
This example simulates an API client that can fail with different, specific error types. The retry policy is configured to only retry on ServerBusyError and give up immediately on NotFoundError.
import { Data, Effect, Schedule } from "effect";
// Define specific, tagged errors for our API client
class ServerBusyError extends Data.TaggedError("ServerBusyError") {}
class NotFoundError extends Data.TaggedError("NotFoundError") {}
let attemptCount = 0;
// A flaky API call that can fail in different ways
const flakyApiCall = Effect.try({
try: () => {
attemptCount++;
const random = Math.random();
if (attemptCount <= 2) {
// First two attempts fail with ServerBusyError (retryable)
console.log(
`Attempt ${attemptCount}: API call failed - Server is busy. Retrying...`
);
throw new ServerBusyError();
}
// Third attempt succeeds
console.log(`Attempt ${attemptCount}: API call succeeded!`);
return { data: "success", attempt: attemptCount };
},
catch: (e) => e as ServerBusyError | NotFoundError,
});
// A predicate that returns true only for the error we want to retry
const isRetryableError = (e: ServerBusyError | NotFoundError) =>
e._tag === "ServerBusyError";
// A policy that retries 3 times, but only if the error is retryable
const selectiveRetryPolicy = Schedule.recurs(3).pipe(
Schedule.whileInput(isRetryableError),
Schedule.addDelay(() => "100 millis")
);
const program = Effect.gen(function* () {
yield* Effect.logInfo("=== Retry Based on Specific Errors Demo ===");
try {
const result = yield* flakyApiCall.pipe(Effect.retry(selectiveRetryPolicy));
yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`);
return result;
} catch (error) {
yield* Effect.logInfo("This won't be reached due to Effect error handling");
return null;
}
}).pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
if (error instanceof NotFoundError) {
yield* Effect.logInfo("Failed with NotFoundError - not retrying");
} else if (error instanceof ServerBusyError) {
yield* Effect.logInfo("Failed with ServerBusyError after all retries");
} else {
yield* Effect.logInfo(`Failed with unexpected error: ${error}`);
}
return null;
})
)
);
// Also demonstrate a case where NotFoundError is not retried
const demonstrateNotFound = Effect.gen(function* () {
yield* Effect.logInfo("\n=== Demonstrating Non-Retryable Error ===");
const alwaysNotFound = Effect.fail(new NotFoundError());
const result = yield* alwaysNotFound.pipe(
Effect.retry(selectiveRetryPolicy),
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logInfo(`NotFoundError was not retried: ${error._tag}`);
return null;
})
)
);
return result;
});
Effect.runPromise(program.pipe(Effect.flatMap(() => demonstrateNotFound)));
Explanation:
Not all errors are created equal. Retrying on a permanent error like "permission denied" or "not found" is pointless and can hide underlying issues. You only want to retry on transient, recoverable errors, such as network timeouts or "server busy" responses.
By adding a predicate to your retry schedule, you gain fine-grained control over the retry logic. This allows you to build much more intelligent and efficient error handling systems that react appropriately to different failure modes. This is a common requirement for building robust clients for external APIs.
Your First Error Handler
- ID: your-first-error-handler
- Skill Level: general
- Use Cases: error-management
Use catchAll or catchTag to recover from errors and keep your program running.
description: Use catchAll or catchTag to recover from errors and keep your program running. globs: "**/*.ts" alwaysApply: true
Your First Error Handler
Rule: Use catchAll or catchTag to recover from errors and keep your program running.
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)
Explanation:
Effect makes errors explicit in your types:
- Errors are typed - You know exactly what can fail
- Handle or propagate - Can't accidentally ignore errors
- Recovery options - Provide fallbacks, retry, or transform
- No try/catch - Declarative error handling
Related Documents
Comprehensive AI Assistant Tools Reference
title: Comprehensive AI Assistant Tools Reference
iOS Deployment Guide
**Introduction:** Deploying the Krome app to iOS (iPhone/iPad) is a bit more involved due to Apple’s ecosystem requirements. This guide will cover setting up an iOS development environment, building the Tauri app for iOS, publishing on Apple’s App Store, alternative distribution options like TestFlight or Enterprise, the App Store review process, common pitfalls, and CI/CD for iOS. As before, we assume you know general development concepts but are new to iOS specifics.
How to Add Resources to Your FastMCP Server
In the Model Context Protocol (MCP), there are three main capabilities:
Continue.dev MCP Integration Setup Guide
Edit your Continue.dev configuration file: