AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Error Handling Patterns

skill-mickeyyaya-refactoring-skills-error-handling-patterns · by mickeyyaya

Use when reviewing code for error handling correctness — covers Result/Either types, Exception hierarchies, Error propagation, Retry with backoff, Circuit breaker, Fail-fast, Graceful degradation, Error boundaries, Null Object/Optional, and Dead Letter Queue with red flags and fix strategies across TypeScript, Go, Rust, Python, and Java

No reviews yet
0 installs
10 views
0.0% view→install

Install

$ agentstack add skill-mickeyyaya-refactoring-skills-error-handling-patterns

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-mickeyyaya-refactoring-skills-error-handling-patterns)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
5mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Error Handling Patterns? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Error Handling Patterns for Code Review

Overview

Swallowed exceptions hide production failures, missing retries cause cascading outages, and overly broad catches mask the real problem. Use this guide during code review to catch error handling hazards before they ship.

When to use: Reviewing code that calls external services, databases, file systems, or async operations; evaluating retry/circuit-breaker logic; any code touching user input or third-party data.

Quick Reference

| Pattern | Core Idea | Primary Red Flag | |---------|-----------|-----------------| | Result/Either | Return success or error as a value | Ignoring returned error values, missing .ok check | | Exception Hierarchies | Structured, typed exception tree | Catching base Exception, throwing generic strings | | Error Propagation | Each layer adds context before forwarding | Swallowing errors, leaking internal details to callers | | Retry with Backoff | Retry transient failures with increasing delays | No backoff, no retry limit, retrying non-idempotent ops | | Circuit Breaker | Stop calling a failing service after threshold | No timeout on HTTP calls, no circuit breaker on dependencies | | Fail-Fast | Validate inputs immediately at boundaries | Deep processing before input validation, late null checks | | Graceful Degradation | Partial results or fallback when dependency fails | Entire request fails because one optional service is down | | Error Boundaries | Contain errors to prevent cascade | Unhandled promise rejections, no global error handler | | Null Object / Optional | Typed absence eliminates null checks | Null returns from methods, unchecked Optional | | Dead Letter Queue | Route unprocessable messages for later analysis | Silently dropping failed messages |


Patterns in Detail

1. Result/Either Types

Red Flags:

  • Returned error values ignored: result, _ := doSomething() in Go
  • Result or Either returned but .ok / .isOk() never checked
  • Mixing exceptions and Result types in the same layer
  • Unwrapping without checking: .unwrap() (Rust) panics on Err

TypeScript:

// BEFORE — throws on failure; caller must know to catch
function parseConfig(raw: string): Config { return JSON.parse(raw); }

// AFTER — error is part of the return type
type Result = { ok: true; value: T } | { ok: false; error: E };
function parseConfig(raw: string): Result {
  try { return { ok: true, value: JSON.parse(raw) as Config }; }
  catch (e) { return { ok: false, error: `Invalid JSON: ${(e as Error).message}` }; }
}
const result = parseConfig(input);
if (!result.ok) { logger.error(result.error); process.exit(1); }

Rust — ? propagates Err automatically:

fn start() -> Result> {
    let config = parse_config(raw_input)?;
    run_server(config)
}

Go — idiomatic (value, error) return:

func parseConfig(raw string) (Config, error) {
    var cfg Config
    if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
        return Config{}, fmt.Errorf("parseConfig: %w", err)
    }
    return cfg, nil
}

2. Exception Hierarchies

Red Flags:

  • Catching Exception / Throwable — also catches OutOfMemoryError
  • throw new Error("something went wrong") — untyped, unclassifiable
  • Catch-and-rethrow without wrapping: loses context, causes duplicate logs
  • Empty catch block: catch (IOException e) {}

Java:

// BEFORE — overly broad; log-and-throw causes duplicate logs
try { return userRepository.findById(id); }
catch (Exception e) { log.error("error", e); throw e; }

// AFTER — typed hierarchy; retryable vs. permanent is explicit
class NotFoundException extends AppException { /* maps to 404 */ }
class ExternalServiceException extends AppException { /* retryable */ }

try {
    return userRepository.findById(id)
        .orElseThrow(() -> new NotFoundException("User not found: " + id));
} catch (DataAccessException e) {
    throw new ExternalServiceException("DB unavailable", e);
}

Python:

class AppError(Exception): pass
class NotFoundError(AppError): pass
class ExternalServiceError(AppError): pass

try:
    user = repo.find_user(user_id)
except NotFoundError: return Response(status=404)
except ExternalServiceError as e:
    logger.warning("unavailable: %s", e); raise

3. Error Propagation

Red Flags:

  • catch (e) { return null; } — error swallowed, caller gets null with no explanation
  • Stack trace lost by re-throwing without chaining the cause
  • Raw SQL, file paths, or stack traces visible in API error responses

Go — context wrapping:

// BEFORE — caller sees "connection refused" with no context
func GetUser(id string) (User, error) { return db.QueryUser(id) }

// AFTER — each layer wraps with what it was doing
func GetUser(id string) (User, error) {
    user, err := db.QueryUser(id)
    if err != nil { return User{}, fmt.Errorf("GetUser(%s): %w", id, err) }
    return user, nil
}

TypeScript — service wraps; controller translates to safe response:

async function getUser(id: string): Promise {
  try { return await userRepo.findById(id); }
  catch (err) { throw new ServiceError(`getUser id=${id}`, { cause: err }); }
}
app.get('/users/:id', async (req, res) => {
  try { res.json(await getUser(req.params.id)); }
  catch (err) {
    logger.error('GET /users/:id', { err });            // full detail for ops
    res.status(500).json({ error: 'Internal error' });  // safe for clients
  }
});

4. Retry with Exponential Backoff

Red Flags:

  • Immediate retry loop with no delay — hammers a struggling service
  • No maximum retry count — infinite loops
  • Retrying non-idempotent operations (POST /charge) — duplicate processing
  • Catching all error types for retry — permanent errors (400, 404) should not be retried

TypeScript:

async function withRetry(
  fn: () => Promise,
  { maxAttempts = 3, baseDelayMs = 200, retryable = (_e: unknown) => true } = {}
): Promise {
  for (let attempt = 1; attempt  setTimeout(r, delay));
    }
  }
  throw new Error('unreachable');
}
const user = await withRetry(() => fetchUser(id), {
  retryable: (e) => e instanceof HttpError && [429, 503].includes(e.status),
});

Python:

def with_retry(fn, max_attempts=3, base_delay=0.2, retryable=lambda e: True):
    for attempt in range(1, max_attempts + 1):
        try: return fn()
        except Exception as e:
            if attempt == max_attempts or not retryable(e): raise
            time.sleep(base_delay * 2 ** (attempt - 1) + random.uniform(0, 0.1))

Cross-reference: concurrency-patterns — Async/Await Pitfalls for fire-and-forget retry tasks.


5. Circuit Breaker

Red Flags:

  • HTTP calls with no timeout — slow dependency hangs threads indefinitely
  • No circuit breaker on microservice calls — one slow service takes down callers
  • No fallback when circuit is open
  • Threshold never tuned for actual traffic patterns

TypeScript:

class CircuitBreaker {
  private failures = 0; private nextAttempt = 0;
  private state: 'closed' | 'open' | 'half-open' = 'closed';
  constructor(private threshold = 5, private cooldownMs = 10_000) {}
  async call(fn: () => Promise): Promise {
    if (this.state === 'open') {
      if (Date.now() = this.threshold) {
        this.state = 'open'; this.nextAttempt = Date.now() + this.cooldownMs;
      }
      throw err;
    }
  }
}

Java — Resilience4j:

Try.ofSupplier(CircuitBreaker.decorateSupplier(CircuitBreaker.ofDefaults("svc"),
    () -> paymentClient.charge(req)))
   .recover(CallNotPermittedException.class, e -> fallbackPayment());

6. Fail-Fast

Red Flags:

  • Validation buried deep in call chain — bad data reaches the database first
  • null/undefined checks scattered through business logic instead of validated at entry
  • No input validation on public API endpoints

TypeScript:

// BEFORE — undefined userId propagates through 3 calls before crashing
async function processOrder(order: Order) {
  return chargeCard((await getUser(order.userId)).card, calculateTotal(order.items));
}

// AFTER — Zod schema rejects bad input immediately
const OrderSchema = z.object({
  userId: z.string().uuid(),
  items: z.array(z.object({ sku: z.string(), qty: z.number().int().positive() })).min(1),
});
async function processOrder(raw: unknown) {
  const order = OrderSchema.parse(raw);
  return chargeCard((await getUser(order.userId)).card, calculateTotal(order.items));
}

Go — guard clauses:

func ProcessOrder(order Order) error {
    if order.UserID == "" { return errors.New("userID required") }
    if len(order.Items) == 0 { return errors.New("items required") }
    return processValidOrder(order)
}

7. Graceful Degradation

Red Flags:

  • Optional service failure causes a 500 on the core endpoint
  • No timeout on optional service calls — slow dependency stalls the entire response
  • Fallback returns misleading data (stale cache not labeled as such)
  • No monitoring when degraded mode is active

TypeScript:

// BEFORE — optional service failure fails the entire endpoint
async function getProduct(id: string): Promise {
  const [product, recs] = await Promise.all([
    productRepo.findById(id),
    recommendationService.getFor(id),  // optional — should not be fatal
  ]);
  return { product, recommendations: recs };
}

// AFTER — required service can throw; optional degrades to fallback
async function getProduct(id: string): Promise {
  const product = await productRepo.findById(id);
  const recommendations = await recommendationService.getFor(id)
    .catch(err => { logger.warn('Recs degraded', { id, err }); return []; });
  return { product, recommendations };
}

8. Error Boundaries

Red Flags:

  • No global unhandledRejection / uncaughtException handler — process crashes silently
  • React component tree with no ErrorBoundary — one render error unmounts the entire UI
  • Message consumer crashes on a bad message without dead-lettering it

TypeScript (Node.js):

process.on('unhandledRejection', (r) => { logger.error('Unhandled rejection', r); process.exit(1); });
process.on('uncaughtException', (e) => { logger.error('Uncaught exception', e); process.exit(1); });
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
  logger.error('Request error', { err, path: req.path });
  res.status(500).json({ error: 'Internal server error' });
});

Java — Spring @ControllerAdvice:

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(NotFoundException.class)
    ResponseEntity notFound(NotFoundException ex) {
        return ResponseEntity.status(404).body(new ErrorResponse(ex.getMessage()));
    }
    @ExceptionHandler(Exception.class)
    ResponseEntity unexpected(Exception ex) {
        log.error("Unexpected", ex);
        return ResponseEntity.status(500).body(new ErrorResponse("Internal error"));
    }
}

Cross-reference: concurrency-patterns — Async/Await Pitfalls for fire-and-forget tasks that bypass error boundaries.


9. Null Object / Optional

Red Flags:

  • Method returns null instead of Optional — callers forget to null-check
  • Optional.get() called without isPresent() — same risk as null dereference
  • ?. chains so long that the failure point is invisible

Java:

// BEFORE — null return; caller forgets check, NPE in production
public User findUser(String id) { return userMap.get(id); }

// AFTER — Optional forces caller to handle absence
public Optional findUser(String id) { return Optional.ofNullable(userMap.get(id)); }
findUser(id).map(User::getName).orElse("Anonymous");

Rust — Option requires exhaustive match:

match find_user(id) {
    Some(user) => process(user),
    None => return Err(AppError::NotFound(id.to_string())),
}

10. Dead Letter Queue

Red Flags:

  • Failed messages acknowledged and discarded — data loss with no audit trail
  • No retry limit — bad message retried indefinitely, blocking the queue
  • DLQ exists but is never monitored — unprocessable messages accumulate silently

TypeScript (SQS):

async function processMessage(msg: SQSMessage): Promise {
  const attempts = Number(msg.Attributes?.ApproximateReceiveCount ?? 1);
  try {
    await handleEvent(JSON.parse(msg.Body));
    await sqs.deleteMessage({ QueueUrl, ReceiptHandle: msg.ReceiptHandle });
  } catch (err) {
    logger.error('Processing failed', { err, attempts });
    if (attempts >= 3) metrics.increment('sqs.dlq.routed');
    // Do NOT delete — SQS will retry and auto-route to DLQ after maxReceiveCount
  }
}

Cross-reference: concurrency-patterns — Producer-Consumer for bounded queue patterns and backpressure.


Error Handling Anti-Patterns

| Anti-Pattern | Description | Fix | |-------------|-------------|-----| | Pokemon Exception Handling | catch(e) {} — catches everything, handles nothing | Handle specific errors; re-throw what you cannot handle | | Error Swallowing | catch(e) { log(e); } without re-throwing | Decide: recover, rethrow, or convert to Result | | String-Typed Errors | throw "something went wrong" | Always throw typed Error objects | | Control Flow via Exceptions | Using try/catch for expected conditions | Use Result types or explicit checks | | Log and Throw | Log then re-throw — duplicate log entries | Log ONCE at the boundary; lower layers wrap without logging | | Overly Broad Catch | Catching Exception when only IOException expected | Catch the most specific type | | Silent Null Return | Returning null from a method that "failed" | Return Optional, Result, or throw typed exception | | Retry Without Idempotency | Retrying POST /charge — double charge | Verify idempotency; use idempotency keys |

Pokemon Exception Handling — TypeScript fix:

// WRONG: try { return await fetchUser(id); } catch (e) {}
// CORRECT — handle expected, propagate the rest
async function loadUser(id: string): Promise {
  try { return await fetchUser(id); }
  catch (err) {
    if (err instanceof NotFoundError) return null;
    throw err;
  }
}

Cross-References

  • concurrency-patterns — Async/Await Pitfalls: unobserved promise rejections and fire-and-forget tasks
  • refactor-functional-patterns — Monadic error handling: map, flatMap, chain over Result/Option types
  • review-code-quality-process — Error handling checklist: patterns here, review workflow there
  • detect-code-smells — "Shotgun Surgery": scattered try/catch blocks indicate a missing centralized error boundary

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.