skills/mav-bp-error-handling/SKILL.md
Error handling conventions for all applications. Covers error propagation, retry strategies, circuit breakers, graceful degradation, error boundaries, and typed errors. Applied when writing or reviewing error handling code.
npx skillsauth add thermiteau/maverick mav-bp-error-handlingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Ensure all error handling is intentional, typed, and resilient. Errors should be caught where they can be acted on, propagated where they cannot, and never swallowed silently.
catch block must either handle, log, or re-throw. An empty catch is a bug.Before applying these standards, load the project-specific error handling implementation:
digraph lookup {
"docs/maverick/skills/error-handling/SKILL.md exists?" [shape=diamond];
"Read and use alongside these standards" [shape=box];
"Invoke upskill" [shape=box];
"Read generated skill" [shape=box];
"docs/maverick/skills/error-handling/SKILL.md exists?" -> "Read and use alongside these standards" [label="yes"];
"docs/maverick/skills/error-handling/SKILL.md exists?" -> "Invoke upskill" [label="no"];
"Invoke upskill" -> "Read generated skill";
"Read generated skill" -> "Read and use alongside these standards";
}
docs/maverick/skills/error-handling/SKILL.mddo-upskill skill with:
catch\s*\(|\.catch\(|except\s|rescue\s|AppError|HttpException|CustomError|BaseError|ErrorBoundary**/error*.*, **/exception*.*, **/errors/**Catch where you can act, propagate where you cannot. Errors should bubble up to the nearest boundary that has enough context to respond.
digraph propagation {
rankdir=TB;
"Low-level function" [shape=box];
"Service layer" [shape=box];
"Error boundary / handler" [shape=box style=filled fillcolor="#ccffcc"];
"Low-level function" -> "Service layer" [label="throw / return error"];
"Service layer" -> "Error boundary / handler" [label="propagate"];
}
null or a default — unless the caller explicitly expects it and the failure is recoverableDefine error types with structured metadata. Do not throw raw strings, generic Error objects, or untyped exceptions.
Every application error should carry:
| Field | Purpose | Example |
| --------- | ----------------------------------------- | ------------------------------------------- |
| type | Machine-readable error classification | VALIDATION_ERROR, NOT_FOUND, CONFLICT |
| message | Human-readable description | User with email already exists |
| context | Structured metadata for diagnosis | { "email": "masked", "userId": "u-123" } |
| cause | Original error (for chained/wrapped errors) | The underlying database or network error |
// GOOD: Typed error with context
class AppError extends Error {
constructor(
public readonly type: string,
message: string,
public readonly context?: Record<string, unknown>,
public readonly cause?: Error
) {
super(message);
}
}
throw new AppError("VALIDATION_ERROR", "Email already registered", {
field: "email",
});
# GOOD: Typed exception with context
class AppError(Exception):
def __init__(self, error_type: str, message: str, context: dict | None = None):
super().__init__(message)
self.error_type = error_type
self.context = context or {}
raise AppError("NOT_FOUND", "Order not found", {"order_id": order_id})
// BAD: Generic string
throw new Error("something went wrong");
// BAD: Bare string
raise "failed"
Transient failures (network timeouts, rate limits, temporary unavailability) should be retried with exponential backoff. Permanent failures (bad input, not found, auth errors) must not be retried.
| Rule | Detail |
| ------------------------- | ------------------------------------------------------------- |
| Max retries | 3 attempts (1 initial + 2 retries) unless configured otherwise |
| Backoff | Exponential: base * 2^attempt (e.g., 100ms, 200ms, 400ms) |
| Jitter | Add random jitter to prevent thundering herd |
| Retryable errors only | Network timeouts, 429, 502, 503, 504. Never retry 400, 401, 403, 404 |
| Idempotency | Only retry operations that are safe to repeat |
When a dependency is failing repeatedly, stop sending requests to it. This prevents cascading failures and gives the dependency time to recover.
digraph circuit {
rankdir=LR;
"Closed" [shape=box style=filled fillcolor="#ccffcc" label="Closed\n(normal)"];
"Open" [shape=box style=filled fillcolor="#ffcccc" label="Open\n(rejecting)"];
"Half-Open" [shape=box style=filled fillcolor="#fff3cc" label="Half-Open\n(testing)"];
"Closed" -> "Open" [label="failure threshold\nexceeded"];
"Open" -> "Half-Open" [label="timeout\nexpired"];
"Half-Open" -> "Closed" [label="probe\nsucceeds"];
"Half-Open" -> "Open" [label="probe\nfails"];
}
| State | Behaviour | | ----------- | -------------------------------------------------------------------- | | Closed | Requests pass through normally. Failures are counted. | | Open | Requests are rejected immediately without calling the dependency. | | Half-Open | A single probe request is allowed through to test if the dependency has recovered. |
When a non-critical dependency fails, degrade the feature rather than crashing the entire application.
| Failing dependency | Full feature | Degraded behaviour | | ------------------------- | ------------------------- | ----------------------------------------- | | Recommendation service | Personalised suggestions | Show popular/default items | | Avatar service | User profile pictures | Show placeholder avatar | | Analytics service | Real-time metrics | Queue events for later, continue normally | | Search service | Full-text search | Fall back to basic filtering | | Notification service | Push notifications | Queue for retry, do not block the action |
For frontend applications, use error boundaries to prevent a failure in one component from crashing the entire application.
componentDidCatch / ErrorBoundary componenterrorCaptured hook / onErrorCaptured composition APIErrorHandler class<svelte:boundary> (Svelte 5+) or handleError hookError responses sent to clients must never include implementation details.
// GOOD: Safe client response
{
"error": {
"type": "VALIDATION_ERROR",
"message": "The email address is not valid.",
"requestId": "req-abc-123"
}
}
// BAD: Leaking internals
{
"error": "QueryFailedError: relation \"users\" does not exist",
"stack": "Error: QueryFailedError\n at /app/src/db/connection.ts:42:11..."
}
Distinguish between errors caused by the client (bad input, missing resource) and errors caused by the server (crashes, dependency failures). They have different handling paths.
| Aspect | Client error (4xx) | Server error (5xx) |
| ---------------- | ------------------------------------------- | -------------------------------------------- |
| Cause | Invalid request from the caller | Failure in the server or its dependencies |
| Retryable | No (unless the client fixes the request) | Possibly (transient failures may recover) |
| Log level | warn (expected, but worth tracking volume) | error (unexpected, requires investigation) |
| Alert | No (unless volume spikes unusually) | Yes (if persistent or above threshold) |
| User message | Specific ("Email is required") | Generic ("Something went wrong") |
Error handling, logging, and alerting are complementary:
| Concern | Role | Skill | | ---------------- | --------------------------------------------------------- | ------------------------------- | | Error handling | Decide what to do with the error (retry, degrade, crash) | This skill | | Logging | Record the error with context for investigation | mav-bp-logging | | Alerting | Notify operations when critical errors demand attention | mav-bp-alerting |
Flow: Error occurs -> Error handling decides the response -> Logging records it -> Alerting notifies (if critical).
| Pattern | Issue | Fix |
| --------------------------------------------------- | ------------------------------ | ------------------------------------------------- |
| Empty catch block | Silently swallowed error | Handle, log, or re-throw |
| catch (err) { return null } | Lost error context | Propagate the error or handle explicitly |
| throw new Error("failed") | Untyped, no context | Use typed error with structured metadata |
| Retrying on 400/401/403 | Retrying permanent failures | Only retry transient errors (429, 5xx, timeouts) |
| No retry on external HTTP calls | Fragile to transient failures | Add retry with exponential backoff |
| Stack trace in API response | Leaking internals | Return safe error with request ID only |
| catch at every layer in the call stack | Duplicate handling/logging | Catch at the boundary, propagate elsewhere |
| Feature crashes app when dependency is down | No graceful degradation | Degrade the feature, keep the app running |
| No error boundary around UI sections | Single component crashes page | Wrap sections in error boundaries with fallback UI |
| Generic error message for all failures | Poor user experience | Distinguish client vs server errors |
development
--- name: do-test description: Write or update tests for a code change. Operates in two modes: `unit` (module-scoped, fast, deterministic) and `integration` (crosses module / service / database boundaries). Intended to be invoked once per testable change from inside a do-issue-* or do-epic phase. Mode is required. argument-hint: mode: unit or integration user-invocable: true disable-model-invocation: false --- **Depends on:** mav-bp-unit-testing, mav-bp-integration-testing, mav-local-verificati
development
Implement a focused code change. Use this skill as the wrapper for any implementation work so the Maverick workflow report captures what was done and so the agent applies the project's coding standards before editing. Intended to be invoked once per task from inside a do-issue-* or do-epic phase, not standalone.
testing
How to stack a PR on top of an unmerged sibling branch, and how to retarget it to the repo's default branch once the sibling merges. Prevents orphan-merge incidents when a dependent story is ready before its parent.
development
Claim, lease, heartbeat, and release protocols for when multiple Claude Code instances may act on the same issue or epic concurrently. GitHub labels and marker comments are the coordination surface; local state is a cache.