Error Handling Skill
Comprehensive, layered error handling for Next.js applications. This file holds the architecture and
contract; copy-paste code is in the references.
Philosophy
- Never expose internal errors to users — log details server-side, show friendly messages client-side.
- Use typed errors —
ServiceError for any service layer, ActionResponse for server actions.
- Fail gracefully — error boundaries, fallback UI, retry mechanisms.
- Log everything — structured logging with context before returning an error.
- Provide feedback — toast notifications for user-facing errors.
The Four Layers
Client → Error Boundaries (React) · Toast notifications · inline form-field errors
↑
Server Action → ActionResponse<T> / RedirectAction · Zod validation with fieldErrors · toast cookies
↑
Service → ServiceError class · tuple pattern [error, data] · categorizeServiceError()
↑
Logging → structured logs · error context (errorCode, isRetryable, userId), logged before return
The service layer is source-agnostic: the same ServiceError contract wraps failures from a
database query, a REST/fetch() API call, or a validation/business-rule check. Nothing below is
tied to a particular data store.
Rules per layer (code in references/examples.md):
- Service layer — always return the tuple
[error, data]: [ServiceError, null] on failure,
[null, data] on success. Wrap external calls in try/catch, run results through
categorizeServiceError(error, resourceName), and log before returning.
- Server action — return
ActionResponse. Validation failures return fieldErrors (from
zod.flatten()) for inline display; service failures set a toast cookie and return a generic
user message (never the raw error).
- Client —
app/error.tsx error boundary logs the error and offers reset(); toasts surface
action feedback; field errors render inline.
ServiceError Contract
ServiceError is the typed error every service-layer operation returns, regardless of where the
failure came from — a database driver/ORM, a fetch()/REST call to another service, or a
validation/business-rule check. It's a plain TypeScript class with no dependency on any data store.
categorizeServiceError(error, resourceName) maps a raw error from any source onto it.
| Property |
Type |
Meaning |
code |
string |
Error code from the neutral vocabulary below |
message |
string |
User-friendly message |
isRetryable |
boolean |
Transient error (network, timeout, service unavailable) |
isNotFound |
boolean |
Resource doesn't exist |
isAlreadyExists |
boolean |
Creating a resource that already exists |
isPermissionDenied |
boolean |
Auth/permission issue |
Neutral error-code vocabulary
code is one of these source-independent values. Each concrete adapter (DB driver, HTTP client,
validator) maps its own raw error codes/HTTP statuses onto this set:
code |
isRetryable |
Typical source |
validation |
no |
Zod parse, business-rule check, HTTP 400/422 |
not-found |
no |
Missing row/document, HTTP 404 |
already-exists |
no |
Unique-constraint violation, HTTP 409 |
permission-denied |
no |
Auth/authorization failure, HTTP 401/403 |
data-corruption |
no |
Record exists but fails its shape/parse |
unavailable |
yes |
Connection refused, service down, HTTP 502/503 |
timeout |
yes |
Deadline exceeded, AbortError, HTTP 504 |
rate-limited |
yes |
Too many requests, HTTP 429 |
external-api |
yes |
Non-specific upstream/third-party failure |
internal |
no |
Unknown/unexpected error |
Factory methods and constructor
ServiceError.notFound("User"); // resource not found
ServiceError.alreadyExists("Budget"); // resource already exists
ServiceError.validation("Invalid input"); // validation / business-rule failed
ServiceError.dataCorruption("Event"); // record exists but data invalid
ServiceError.permissionDenied("Budget"); // auth/permission issue (resource optional)
ServiceError.internal("Budget"); // unknown/unexpected failure
// Full constructor for any other code (e.g. a retryable upstream failure):
// new ServiceError(code: string, message: string, isRetryable = false)
new ServiceError("external-api", "Payment service unavailable", true);
Error Response Flow
source error (DB / fetch / validation) → categorizeServiceError() → log with context → return [error, null] → server action inspects error flags → set toast cookie → return ActionResponse with a generic message → client shows toast.
Related Skills
firebase-firestore — one concrete adapter that maps a specific data store's raw errors onto this contract.
server-actions — ActionResponse types and patterns.
toast-notifications — user feedback via toasts.
structured-logging — structured logging patterns; see its
references/setup.md for the logger itself and required libraries.
1---2name: error-handling3description: Comprehensive error handling for Next.js applications — use whenever adding or fixing error handling, building database or service layer operations, writing server actions, or making an app production-ready. Covers ServiceError (typed error contract for any external service: DB, API, network), tuple return pattern [error, data], ActionResponse for server actions, React error boundaries, toast notifications, retry with exponential backoff, and circuit breaker patterns. Trigger on: "add error handling", "handle database errors", "handle service errors", "retry failed requests", "error boundary", "handle action errors", "toast on error", "log errors", "circuit breaker", "graceful degradation", "categorizeServiceError", "ServiceError".4---56# Error Handling Skill78Comprehensive, layered error handling for Next.js applications. This file holds the architecture and9contract; copy-paste code is in the references.1011> - [references/patterns.md](./references/patterns.md) — error-handling patterns by layer.12> - [references/examples.md](./references/examples.md) — full worked examples (complete CRUD with errors).13> - [references/retry-patterns.md](./references/retry-patterns.md) — retry, circuit breaker, resilience.14> - [references/validation-vs-runtime.md](./references/validation-vs-runtime.md) — validation vs runtime errors.1516## Philosophy17181. **Never expose internal errors to users** — log details server-side, show friendly messages client-side.192. **Use typed errors** — `ServiceError` for any service layer, `ActionResponse` for server actions.203. **Fail gracefully** — error boundaries, fallback UI, retry mechanisms.214. **Log everything** — structured logging with context before returning an error.225. **Provide feedback** — toast notifications for user-facing errors.2324## The Four Layers2526```27Client → Error Boundaries (React) · Toast notifications · inline form-field errors28 ↑29Server Action → ActionResponse<T> / RedirectAction · Zod validation with fieldErrors · toast cookies30 ↑31Service → ServiceError class · tuple pattern [error, data] · categorizeServiceError()32 ↑33Logging → structured logs · error context (errorCode, isRetryable, userId), logged before return34```3536The **service layer is source-agnostic**: the same `ServiceError` contract wraps failures from a37database query, a REST/`fetch()` API call, or a validation/business-rule check. Nothing below is38tied to a particular data store.3940Rules per layer (code in [references/examples.md](./references/examples.md)):4142- **Service layer** — always return the tuple `[error, data]`: `[ServiceError, null]` on failure,43 `[null, data]` on success. Wrap external calls in `try/catch`, run results through44 `categorizeServiceError(error, resourceName)`, and log before returning.45- **Server action** — return `ActionResponse`. Validation failures return `fieldErrors` (from46 `zod.flatten()`) for inline display; service failures set a toast cookie and return a **generic**47 user message (never the raw error).48- **Client** — `app/error.tsx` error boundary logs the error and offers `reset()`; toasts surface49 action feedback; field errors render inline.5051## ServiceError Contract5253`ServiceError` is the typed error every service-layer operation returns, **regardless of where the54failure came from** — a database driver/ORM, a `fetch()`/REST call to another service, or a55validation/business-rule check. It's a plain TypeScript class with no dependency on any data store.56`categorizeServiceError(error, resourceName)` maps a raw error from any source onto it.5758| Property | Type | Meaning |59| -------------------- | ------- | ----------------------------------------------------------- |60| `code` | string | Error code from the neutral vocabulary below |61| `message` | string | User-friendly message |62| `isRetryable` | boolean | Transient error (network, timeout, service unavailable) |63| `isNotFound` | boolean | Resource doesn't exist |64| `isAlreadyExists` | boolean | Creating a resource that already exists |65| `isPermissionDenied` | boolean | Auth/permission issue |6667### Neutral error-code vocabulary6869`code` is one of these source-independent values. Each concrete adapter (DB driver, HTTP client,70validator) maps its own raw error codes/HTTP statuses onto this set:7172| `code` | `isRetryable` | Typical source |73| ----------------- | ------------- | ------------------------------------------------------- |74| `validation` | no | Zod parse, business-rule check, HTTP 400/422 |75| `not-found` | no | Missing row/document, HTTP 404 |76| `already-exists` | no | Unique-constraint violation, HTTP 409 |77| `permission-denied` | no | Auth/authorization failure, HTTP 401/403 |78| `data-corruption` | no | Record exists but fails its shape/parse |79| `unavailable` | **yes** | Connection refused, service down, HTTP 502/503 |80| `timeout` | **yes** | Deadline exceeded, `AbortError`, HTTP 504 |81| `rate-limited` | **yes** | Too many requests, HTTP 429 |82| `external-api` | **yes** | Non-specific upstream/third-party failure |83| `internal` | no | Unknown/unexpected error |8485### Factory methods and constructor8687```typescript88ServiceError.notFound("User"); // resource not found89ServiceError.alreadyExists("Budget"); // resource already exists90ServiceError.validation("Invalid input"); // validation / business-rule failed91ServiceError.dataCorruption("Event"); // record exists but data invalid92ServiceError.permissionDenied("Budget"); // auth/permission issue (resource optional)93ServiceError.internal("Budget"); // unknown/unexpected failure9495// Full constructor for any other code (e.g. a retryable upstream failure):96// new ServiceError(code: string, message: string, isRetryable = false)97new ServiceError("external-api", "Payment service unavailable", true);98```99100## Error Response Flow101102`source error (DB / fetch / validation) → categorizeServiceError() → log with context →103return [error, null] → server action inspects error flags → set toast cookie → return ActionResponse104with a generic message → client shows toast`.105106## Related Skills107108- `firebase-firestore` — one concrete adapter that maps a specific data store's raw errors onto this contract.109- `server-actions` — `ActionResponse` types and patterns.110- `toast-notifications` — user feedback via toasts.111- `structured-logging` — structured logging patterns; see its112 [references/setup.md](../structured-logging/references/setup.md) for the logger itself and required libraries.