Handle Errors with catchTag, catchTags, and catchAll
Guideline
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.
Rationale
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.
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.