error-handler-pattern · diff
git:20260824.9c2e24b to git:20260824.63449ef
36 added, 197 removed. Audit A to A.
---
name: error-handler-pattern
- description: Chuẩn hóa error handling — typed errors, unified HTTP response schema, logging strategy, retry logic
+ description: Chuẩn hóa error handling theo Architecture Profile: typed error, response schema, logging và retry
user-invocable: true
---
- # Skill: Error Handler Pattern (`/error-handler-pattern`)
+ # Error Handler Pattern (`/error-handler-pattern`)
- Sử dụng skill này để chuẩn hóa error handling toàn codebase theo `ENG-02` trong `CONSTITUTION.md`. Đóng gói tri thức Senior Engineer về error design.
+ Audit hoặc thiết kế error handling theo `CONSTITUTION.md` `ENG-02`.
## Tham số
- - `--file=<path>`: File cần audit/fix error handling.
- - `--feature=<slug>`: Audit toàn bộ feature.
- - `--mode=audit|scaffold`: `audit` (phân tích gaps), `scaffold` (tạo error class template).
- ---
-
- ## 1. Error Taxonomy (Phân loại lỗi)
-
- | Error Type | HTTP Code | Retry? | Log Level | Example |
- | :------------------- | :-------- | :------ | :-------- | :------------------------------- |
- | `ValidationError` | 400 | No | `warn` | Invalid email format |
- | `AuthenticationError`| 401 | No | `warn` | JWT expired / missing |
- | `ForbiddenError` | 403 | No | `warn` | Insufficient permissions |
- | `NotFoundError` | 404 | No | `info` | Resource not found |
- | `ConflictError` | 409 | No | `warn` | Duplicate email / idempotency |
- | `RateLimitError` | 429 | Yes | `warn` | Too many requests |
- | `ExternalServiceError`| 502/503 | Yes | `error` | Payment gateway timeout |
- | `InternalError` | 500 | No | `error` | Unhandled exception |
-
- ---
+ - `--file=<path>`: File cần audit/fix.
+ - `--feature=<slug>`: Audit toàn feature.
+ - `--mode=audit|scaffold`: `audit` phân tích gap; `scaffold` tạo template phù hợp profile.
- ## 2. Unified Error Response Schema (ENG-02)
+ ## Architecture Profile gate
- Mọi error response PHẢI tuân thủ schema sau (không expose stack trace cho client):
+ - Core-only được audit error taxonomy và layer boundary.
+ - HTTP handler, request ID, logger adapter, package import, retry client config và test command chỉ được sinh khi binding `APPROVED` có evidence.
+ - Binding thiếu thì báo `CONFIGURATION GAP`; không sinh Express, NestJS, Fastify, logger package, test command hoặc retry API suy đoán.
- ```typescript
- // ✅ Standard error response
- {
- "error_code": "VALIDATION_FAILED", // Machine-readable code
- "message": "Email is invalid", // Human-readable, safe to show
- "request_id": "req_abc123", // Tracing ID
- "timestamp": "2026-08-24T08:00:00Z", // ISO 8601 UTC
- "details": [ // Optional: field-level errors
- { "field": "email", "issue": "Invalid format" }
- ]
- }
- ```
+ ## Quy tắc
- ---
+ - Domain/usecase throw typed error, không import HTTP/transport type.
+ - Interface map typed error thành response theo approved transport adapter.
+ - Infra translate provider/DB error tại boundary, không leak driver detail hay stack.
+ - Khi HTTP binding đã `APPROVED`, response theo `ENG-02` là `{ error_code, message, request_id, timestamp }`; `details` không chứa PII.
+ - Với event/CLI/transport khác, error contract do `SPEC.md` và binding đã approved xác định; `request_id` chỉ lấy từ request-context mechanism đã approved.
+ - Retry chỉ ở `src/infra/`, chỉ cho idempotent operation hoặc operation có idempotency guarantee; timeout/backoff phải do Spec/constraint xác định.
- ## 3. Typed Error Base Classes (Template)
+ ## Core template
```typescript
- // src/shared/errors/base-error.ts
-
export abstract class AppError extends Error {
- abstract readonly statusCode: number;
abstract readonly errorCode: string;
- readonly isOperational: boolean = true; // vs programmer errors
+ readonly isOperational = true;
constructor(
message: string,
readonly details?: Record<string, unknown>
) {
super(message);
this.name = this.constructor.name;
- Error.captureStackTrace(this, this.constructor);
}
}
-
- export class ValidationError extends AppError {
- readonly statusCode = 400;
- readonly errorCode = 'VALIDATION_FAILED';
- }
-
- export class AuthenticationError extends AppError {
- readonly statusCode = 401;
- readonly errorCode = 'AUTHENTICATION_REQUIRED';
- }
-
- export class ForbiddenError extends AppError {
- readonly statusCode = 403;
- readonly errorCode = 'FORBIDDEN';
- }
-
- export class NotFoundError extends AppError {
- readonly statusCode = 404;
- readonly errorCode = 'NOT_FOUND';
- }
-
- export class ConflictError extends AppError {
- readonly statusCode = 409;
- readonly errorCode = 'CONFLICT';
- }
-
- export class ExternalServiceError extends AppError {
- readonly statusCode = 502;
- readonly errorCode = 'EXTERNAL_SERVICE_FAILED';
- readonly isOperational = false; // Trigger alert
- }
```
- ---
-
- ## 4. Global Error Handler Middleware
-
- ```typescript
- // src/interface/middleware/global-error-handler.ts
-
- import { Request, Response, NextFunction } from 'express';
- import { AppError } from '@/shared/errors/base-error';
- import { logger } from '@/shared/logger';
-
- export function globalErrorHandler(
- err: Error,
- req: Request,
- res: Response,
- _next: NextFunction
- ): void {
- const requestId = req.id as string;
-
- if (err instanceof AppError) {
- // Operational error — expected, safe to send details
- logger[err.statusCode >= 500 ? 'error' : 'warn']({
- event: 'OPERATIONAL_ERROR',
- errorCode: err.errorCode,
- message: err.message,
- requestId,
- path: req.path,
- });
-
- res.status(err.statusCode).json({
- error_code: err.errorCode,
- message: err.message,
- request_id: requestId,
- timestamp: new Date().toISOString(),
- ...(err.details && { details: err.details }),
- });
- return;
- }
-
- // Programmer error — never expose internals
- logger.error({
- event: 'UNHANDLED_ERROR',
- error: err.message,
- stack: err.stack, // Log stack server-side only
- requestId,
- path: req.path,
- });
-
- res.status(500).json({
- error_code: 'INTERNAL_SERVER_ERROR',
- message: 'An unexpected error occurred. Please try again later.',
- request_id: requestId,
- timestamp: new Date().toISOString(),
- });
- }
- ```
-
- ---
-
- ## 5. Retry Logic Pattern (cho External Services)
-
- ```typescript
- // src/shared/utils/retry-with-backoff.ts
-
- interface RetryOptions {
- maxAttempts?: number; // Default: 3
- baseDelayMs?: number; // Default: 500ms
- maxDelayMs?: number; // Default: 5000ms
- shouldRetry?: (err: Error) => boolean;
- }
-
- export async function retryWithBackoff<T>(
- fn: () => Promise<T>,
- opts: RetryOptions = {}
- ): Promise<T> {
- const { maxAttempts = 3, baseDelayMs = 500, maxDelayMs = 5000 } = opts;
- const shouldRetry = opts.shouldRetry ?? ((err) => err instanceof ExternalServiceError);
-
- let lastError: Error;
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
- try {
- return await fn();
- } catch (err) {
- lastError = err as Error;
- if (attempt === maxAttempts || !shouldRetry(lastError)) throw lastError;
- const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs);
- await new Promise((r) => setTimeout(r, delay)); // Exponential backoff
- }
- }
- throw lastError!;
- }
- ```
-
- ---
-
- ## 6. Audit Checklist
-
- - [ ] Tất cả `throw new Error()` đã được thay bằng typed `AppError` subclass
- - [ ] Global error handler đăng ký là middleware cuối cùng
- - [ ] KHÔNG có `try/catch` nuốt lỗi mà không log hoặc re-throw
- - [ ] KHÔNG expose `err.stack` trong HTTP response
- - [ ] External service calls đều có retry + timeout
- - [ ] Error codes nhất quán, dùng được cho i18n translation
-
- ---
+ Đặt tại `src/shared/errors/`. Chỉ map `errorCode` sang HTTP/event status ở approved interface adapter.
- ## Output Format
+ ## Output
+ ```text
+ ERROR HANDLING AUDIT REPORT
+ Feature: {slug} | Profile: v{version}
+ CRITICAL:
+ [ENG-02] {path}:{line} — {exposed stack/PII/provider error or missing safe mapping}
+ WARNING:
+ [BOUNDARY|RETRY|LOGGING] {path}:{line} — {finding}
+ CONFIGURATION GAP:
+ {missing binding; framework-specific remediation omitted}
+ REMEDIATION:
+ - Binding: {approved binding or human decision required}
+ - Change: {profile-compatible action}
+ - Verification: {exact approved command or N/A with reason}
+ - Spec/Plan impact: {artifact or N/A}
```
- ⚠️ ERROR HANDLING AUDIT REPORT
- ═══════════════════════════════
- 🔴 MISSING:
- - orders.service.ts:67 — throw new Error('not found') → dùng NotFoundError
- - payment.service.ts:134 — Swallowed catch: catch(e) {} — mất error info
- 🟡 INCONSISTENT:
- - 3 files dùng { error: message } thay vì { error_code, message, request_id }
-
- 🟢 OK:
- ✓ Global error handler đã có
- ✓ Retry logic cho payment gateway
-
- 📋 ACTION PLAN:
- 1. Tạo src/shared/errors/ với base-error.ts (scaffold mode: /error-handler-pattern --mode=scaffold)
- 2. Replace 12 throw new Error() instances
- 3. Standardize response schema ở global handler
- ```
+ Error code, visible message, retry hoặc idempotency change cần cập nhật `SPEC.md` trước code. Chỉ thay đổi chính `ENG-02` trong `CONSTITUTION.md` mới cần RFC; thay đổi feature dùng `/sdd-review` trước execution.