error-handling-pattern-3-custom-error-strategies

error-handling-pattern-3-custom-error-strategies is a cursor rule for Cursor from PaulJPhilp/EffectPatterns. It costs 2,660 tokens per session, scanned A, original, MIT.

A pattern for assigning different recovery actions to named errors, such as retrying a temporary network failure or rejecting invalid input.

In plain words
What is it for?
Use it to model network, validation, authentication, permission, rate-limit, and not-found handling in TypeScript Effect programs.
Why use it?
It makes recovery depend on the actual problem instead of applying the same response to every failure.

Cursor rule for Cursor

Written for Cursor: a Cursor rule (.mdc).

Install

Getting it into your agent

One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.

agentmods
npx agentmods add rules/pauljphilp/effectpatterns/error-handling-pattern-3-custom-error-strategies
Clone the repo
git clone --depth 1 https://github.com/PaulJPhilp/EffectPatterns

Made for: Cursor.

Wrote this? Show the measurements

A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.

agentmods badge for error-handling-pattern-3-custom-error-strategies

README.md
[![agentmods](https://agentmods.dev/badge/rules/pauljphilp/effectpatterns/error-handling-pattern-3-custom-error-strategies.svg)](https://agentmods.dev/rules/pauljphilp/effectpatterns/error-handling-pattern-3-custom-error-strategies)
Your own site
<a href="https://agentmods.dev/rules/pauljphilp/effectpatterns/error-handling-pattern-3-custom-error-strategies"><img src="https://agentmods.dev/badge/rules/pauljphilp/effectpatterns/error-handling-pattern-3-custom-error-strategies.svg" alt="Measured on agentmods" height="20"></a>
Per session 2,660 This file is loaded in full into every session.
When invoked 2,660 The same file — it is already loaded in full.
Security scan A 0 findings. Scan, not verified.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5.1 $0.02660 $0.02660
Opus 5 $0.01330 $0.01330
Sonnet 5 $0.00532 $0.00532
Haiku 4.5 $0.00266 $0.00266

Measured 3d ago against content hash 1b3cd1a46a02, method: parsed. Prices are Anthropic first-party input rates as of 2026-09-06, from the pricing page.

Security

Grade A, and why

error-handling-pattern-3-custom-error-strategies scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 3d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Nothing flagged

None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.

content/published/rules/cursor/error-handling-pattern-3-custom-error-strategies.mdc · 461 lines

How it starts

The opening of the file, as written. The whole thing — 461 lines — stays where its author put it; the contents beside it link to each section on GitHub.

description: Use tagged errors and custom error types to enable type-safe error handling and business-logic-aware recovery strategies. globs: "**/*.ts" alwaysApply: true

Error Handling Pattern 3: Custom Error Strategies

Rule: Use tagged errors and custom error types to enable type-safe error handling and business-logic-aware recovery strategies.

Example

This example demonstrates custom error strategies.

import { Effect, Data, Schedule } from "effect";

// Custom domain errors
class NetworkError extends Data.TaggedError("NetworkError")<{
  endpoint: string;
  statusCode?: number;
  retryable: boolean;
}> {}

class ValidationError extends Data.TaggedError("ValidationError")<{
  field: string;
  reason: string;
}> {}

class AuthenticationError extends Data.TaggedError("AuthenticationError")<{
  reason: "invalid-token" | "expired-token" | "missing-token";
}> {}

class PermissionError extends Data.TaggedError("PermissionError")<{
  resource: string;
  action: string;
}> {}

class RateLimitError extends Data.TaggedError("RateLimitError")<{
  retryAfter: number; // milliseconds
}> {}

class NotFoundError extends Data.TaggedError("NotFoundError")<{
  resource: string;
  id: string;
}> {}

// Recovery strategy selector
const selectRecoveryStrategy = (
  error: Error
): "retry" | "fallback" | "fail" | "user-message" => {
  if (error instanceof NetworkError && error.retryable) {
    return "retry";
  }

  if (error instanceof RateLimitError) {
    return "retry"; // With backoff
  }

  if (error instanceof ValidationError) {
    return "user-message"; // User can fix
  }

  if (error instanceof NotFoundError) {
    return "fallback"; // Use empty result
  }

  if (
    error instanceof AuthenticationError &&
    error.reason === "expired-token"
  ) {
    return "retry"; // Refresh token
  }

  if (error instanceof PermissionError) {
    return "fail"; // Don't retry
  }

  return "fail"; // Default: don't retry
};

const program = Effect.gen(function* () {
  console.log(
    `\n[CUSTOM ERROR STRATEGIES] Domain-aware error handling\n`
  );

  // Example 1: Type-safe error handling
  console.log(`[1] Type-safe error catching:\n`);

  const operation1 = Effect.fail(
    new ValidationError({
      field: "email",
      reason: "Invalid format",
    })
  );

  const handled1 = operation1.pipe(
    Effect.catchTag("ValidationError", (error) =>
      Effect.gen(function* () {
        yield* Effect.log(`[CAUGHT] Validation error`);
        yield* Effect.log(`  Field: ${error.field}`);
        yield* Effect.log(`  Reason: ${error.reason}\n`);

        return "validation-failed";
      })
    )
  );

  yield* handled1;

  // Example 2: Multiple error types with different recovery
  console.log(`[2] Different recovery per error type:\n`);

  interface ApiResponse {
    status: number;
    body?: unknown;
  }

  const callApi = (shouldFail: "network" | "validation" | "ratelimit" | "success") =>
    Effect.gen(function* () {
      switch (shouldFail) {
        case "network":
          yield* Effect.fail(
            new NetworkError({
              endpoint: "https://api.example.com/data",
              statusCode: 503,
              retryable: true,
            })
          );

        case "validation":
          yield* Effect.fail(
            new ValidationError({
              field: "id",
              reason: "Must be numeric",
            })
          );

        case "ratelimit":
          yield* Effect.fail(
            new RateLimitError({
              retryAfter: 5000,
            })
          );

        case "success":
          return { status: 200, body: { id: 123 } };
      }
    });

  // Test each error type
  const testCases = ["network", "validation", "ratelimit", "success"] as const;

  for (const testCase of testCases) {
    const strategy = yield* callApi(testCase).pipe(
      Effect.catchTag("NetworkError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[NETWORK] Retryable: ${error.retryable}, Status: ${error.statusCode}`
          );

          return "will-retry";
        })
      ),
      Effect.catchTag("ValidationError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[VALIDATION] ${error.field}: ${error.reason} (no retry)`
          );

          return "user-must-fix";
        })
      ),
      Effect.catchTag("RateLimitError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[RATE-LIMIT] Retry after ${error.retryAfter}ms`
          );

          return "retry-with-backoff";
        })
      ),
      Effect.catchAll((error) =>
        Effect.gen(function* () {
          yield* Effect.log(`[SUCCESS] Got response`);

          return "completed";
        })
      )
    );

    yield* Effect.log(`  Strategy: ${strategy}\n`);
  }

  // Example 3: Custom retry strategy based on error
  console.log(`[3] Error-specific retry strategies:\n`);

  let attemptCount = 0;

  const networkOperation = Effect.gen(function* () {
    attemptCount++;

    yield* Effect.log(`[ATTEMPT] ${attemptCount}`);

    if (attemptCount === 1) {
      yield* Effect.fail(
        new NetworkError({
          endpoint: "api.example.com",
          statusCode: 502,
          retryable: true,
        })
      );
    }

    if (attemptCount === 2) {
      yield* Effect.fail(
        new RateLimitError({
          retryAfter: 100,
        })
      );
    }

    return "success";
  });

  // Type-safe retry with error classification
  let result3: string | null = null;

  for (let i = 0; i < 3; i++) {
    result3 = yield* networkOperation.pipe(
      Effect.catchTag("NetworkError", (error) =>
        Effect.gen(function* () {
          if (error.retryable && i < 2) {
            yield* Effect.log(`[RETRY] Network error is retryable`);

            return null; // Signal to retry
          }

          yield* Effect.log(`[FAIL] Network error not retryable`);

          return Effect.fail(error);
        })
      ),
      Effect.catchTag("RateLimitError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[BACKOFF] Rate limited, waiting ${error.retryAfter}ms`
          );

          yield* Effect.sleep(`${error.retryAfter} millis`);

          return null; // Signal to retry
        })
      ),
      Effect.catchAll((error) =>
        Effect.gen(function* () {
          yield* Effect.log(`[ERROR] Unhandled: ${error}`);

          return Effect.fail(error);
        })
      )
    ).pipe(
      Effect.catchAll(() => Effect.succeed(null))
    );

    if (result3 !== null) {
      break;
    }
  }

  yield* Effect.log(`\n[RESULT] ${result3}\n`);

  // Example 4: Error-aware business logic
  console.log(`[4] Business logic with error handling:\n`);

  interface User {
    id: string;
    email: string;
  }

  const loadUser = (id: string): Effect.Effect<User, NetworkError | NotFoundError> =>
    Effect.gen(function* () {
      if (id === "invalid") {
        yield* Effect.fail(
          new NotFoundError({
            resource: "user",
            id,
          })
        );
      }

      if (id === "network-error") {
        yield* Effect.fail(
          new NetworkError({
            endpoint: "/api/users",
            retryable: true,
          })
        );
      }

      return { id, email: `user-${id}@example.com` };
    });

  const processUser = (id: string) =>
    loadUser(id).pipe(
      Effect.catchTag("NotFoundError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[BUSINESS] User not found: ${error.id}`
          );

          // Return default/empty user
          return { id: "", email: "" };
        })
      ),
      Effect.catchTag("NetworkError", (error) =>
        Effect.gen(function* () {
          yield* Effect.log(
            `[BUSINESS] Network error, will retry from cache`
          );

          return { id, email: "[email protected]" };
        })
      )
    );

  yield* processUser("valid-id");

  yield* processUser("invalid");

  yield* processUser("network-error");

  // Example 5: Discriminated union for exhaustiveness
  console.log(`\n[5] Exhaustiveness checking (compile-time safety):\n`);

  const classifyError = (
    error: NetworkError | ValidationError | AuthenticationError | PermissionError
  ): string => {
    switch (error._tag) {
      case "NetworkError":
        return `network: ${error.statusCode}`;

      case "ValidationError":
        return `validation: ${error.field}`;

      case "AuthenticationError":
        return `auth: ${error.reason}`;

      case "PermissionError":
        return `permission: ${error.action}`;

      // TypeScript ensures all cases covered
      default:
        const _exhaustive: never = error;
        return _exhaustive;
    }
  };

  const testError = new ValidationError({
    field: "age",
    reason: "Must be >= 18",
  });

  const classification = classifyError(testError);

  yield* Effect.log(`[CLASSIFY] ${classification}`);

  // Example 6: Recovery strategy chains
  console.log(`\n[6] Chained recovery strategies:\n`);

  const resilientOperation = Effect.gen(function* () {
    yield* Effect.fail(
      new RateLimitError({
        retryAfter: 50,
      })
    );
  });

  const withRecovery = resilientOperation.pipe(
    Effect.catchTag("RateLimitError", (error) =>
      Effect.gen(function* () {
        yield* Effect.log(
          `[STEP 1] Caught rate limit, waiting ${error.retryAfter}ms`
        );

        yield* Effect.sleep(`${error.retryAfter} millis`);

        // Try again
        return yield* Effect.succeed("recovered");
      })
    ),
    Effect.catchTag("NetworkError", (error) =>
      Effect.gen(function* () {
        if (error.retryable) {
          yield* Effect.log(`[STEP 2] Network error, retrying...`);

          return "retry";
        }

        return yield* Effect.fail(error);
      })
    ),
    Effect.catchAll((error) =>
      Effect.gen(function* () {
        yield* Effect.log(`[STEP 3] Final fallback`);

        return "fallback";
      })
    )
  );

  yield* withRecovery;
});

Effect.runPromise(program);

Read the full file on GitHub · 461 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 3d ago First seen · 461 lines · 2,660 tokens per session scan A 1b3cd1a46a02

Subscribe to this mod's changes

error-handling-pattern-3-custom-error-strategies is a cursor rule published in the GitHub repository PaulJPhilp/EffectPatterns (796 stars, last pushed 2mo ago), licensed MIT. It adds 2,660 tokens to every session, about $0.0133 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-09-03.