agentleFS
jars-demojars-demo/jars-skills/skills/error-handling/SKILL.md

error-handling

Skill0 starsChanged 32 days agoMITView on GitHub

Add and improve error handling in web applications and APIs. Use when the user asks to improve error handling, add a global error handler, create custom error classes, fix unhandled promise rejections, return consistent error responses, stop leaking stack traces to clients, handle async errors, or make errors easier to debug in production.

80 other projects use a version of this, 2 of them edited.

Adopted by

Most recently taken up by

Used with

Projects with it also use convention-learner, opentelemetry and 5 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • codewithmukeshSkill

    convention-learner

    Detects and enforces project-specific coding conventions by analyzing existing codebase patterns. Learns naming conventions, folder structure, test organization, and coding style from the existing code. Load when: "conventions", "coding standards", "project patterns", "enforce style", "detect patterns", "learn conventions", "code consistency".

    Also in 3 projects that use this

  • codewithmukeshSkill

    opentelemetry

    OpenTelemetry observability for .NET 10 applications. Covers traces, metrics, and logs using the OpenTelemetry SDK with OTLP export. Includes custom ActivitySource, IMeterFactory metrics, resource configuration, and Aspire Dashboard integration. Load this skill when setting up distributed tracing, custom metrics, OTLP export, or when the user mentions "OpenTelemetry", "OTLP", "traces", "spans", "Activity", "ActivitySource", "metrics", "IMeterFactory", "Meter", "Counter", "Histogram", "Gauge", "telemetry", "observability", "distributed tracing", "OTEL", or "Aspire Dashboard".

    Also in 3 projects that use this

  • MovGP0Skill

    modern-csharp

    Modern C# style guidance for Bendex and MicroSea code changes, including primary constructors, expression-bodied members, var usage, target-typed new, lambdas, and pattern matching.

    Also in 3 projects that use this

  • MicrosoftSkill

    resilience

    Apply resilience patterns (retry, backoff, circuit breaking) when writing or reviewing code that makes outbound HTTP/REST calls, database operations, or any I/O that can experience transient failures. Use this skill when: (1) writing new code that calls external services or APIs, (2) reviewing code that uses fetch, HTTP clients, or service-to-service calls, (3) adding queue consumers or workers that interact with remote systems, (4) debugging intermittent failures in production.

    Also in 4 projects that use this

  • junhjangSkill

    configuration

    Adding a new config file, adding a field, or wiring a config consumer. Enforces schema validation at startup (no permissive defaults), env-var indirection for secrets, layered config (defaults / per-env / operator-override), hot-reload explicit per field.

    Also in 4 projects that use this

  • MicrosoftSkill

    authentication

    Wire this in whenever the app uses or connects to data — records, a database, entities, per-user data, row-level security — or when the user wants sign-in, accounts, login, or protected pages. Rayfin data is always accessed as an authenticated user, so **data implies auth**; only a static page over public data skips it. The starter ships as a no-auth "hello world" that previews with no backend, and the full Fabric auth scaffolding is already in the project — this skill wires it in: AuthProvider + bootstrapAuth in main.tsx, the route guard in App.tsx, the sign-in page, sign-out, and the env/deploy requirement. Triggers: auth, authentication, sign in, sign-in, login, log in, sign out, logout, account, user, identity, protect route, require login, gated page, data, database, records, per-user, row-level security, who is the current user, Fabric auth, Entra.

    Also in 5 projects that use this

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 64 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Read the existing error handling before adding to it: Check for a global handler, custom error classes, and existing response shapes.

    line 14
  2. Distinctive:

    Every error response from the API must have the same shape so clients can handle errors generically:

    line 135
  3. Very specific:

    Never return 500 Internal Server Error with the actual exception message.

    line 152

“Common” is measured: kept by at least 6 in 10 of the 64 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Error handling

Handle errors at the right layer, expose the right amount of detail to the client, and always give operators enough context to diagnose what went wrong. Never swallow errors silently and never leak internal details to users.

Ground rules

  • Read the existing error handling before adding to it. Check for a global handler, custom error classes, and existing response shapes. Don't introduce a second pattern.
  • Never swallow errors silently. A bare catch {} or except: pass that does nothing is always wrong. At minimum, log and re-throw.
  • Never send stack traces or internal error messages to the client. Log them server-side; return a safe, generic message to the caller.
  • Distinguish operational errors from programmer errors. Operational errors (invalid input, not found, auth failure) are expected and handled gracefully. Programmer errors (null dereference, assertion failure) should crash the process or alert — not be silently caught.
  • Fail loudly in development, gracefully in production.

Step 0: Read the existing setup

# Find existing error classes or handlers
grep -r "class.*Error\|ErrorHandler\|error.*middleware\|app\.use.*err\|exception_handler\|HTTPException" \
  --include="*.ts" --include="*.js" --include="*.py" --include="*.go" -l | head -10

# Find swallowed errors
grep -rn "catch\s*{}\|catch.*{\s*}\|except:\s*pass\|except Exception:\s*$" \
  --include="*.ts" --include="*.js" --include="*.py" | head -20

Custom error classes

Define a hierarchy once. Every thrown error should be an instance of a known class so handlers can make decisions without parsing message strings.

Node.js / TypeScript

// Base class — all app errors extend this
export class AppError extends Error {
  constructor(
    public readonly message: string,
    public readonly statusCode: number,
    public readonly code: string,       // machine-readable, e.g. "USER_NOT_FOUND"
    public readonly isOperational = true // false = programmer error → crash
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

// Specific error types
export class NotFoundError extends AppError {
  constructor(resource: string, id?: string) {
    super(
      id ? `${resource} '${id}' not found` : `${resource} not found`,
      404,
      "NOT_FOUND"
    );
  }
}

export class ValidationError extends AppError {
  constructor(
    message: string,
    public readonly fields?: Record<string, string>
  ) {
    super(message, 400, "VALIDATION_ERROR");
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = "Authentication required") {
    super(message, 401, "UNAUTHORIZED");
  }
}

export class ForbiddenError extends AppError {
  constructor(message = "Access denied") {
    super(message, 403, "FORBIDDEN");
  }
}

export class ConflictError extends AppError {
  constructor(message: string) {
    super(message, 409, "CONFLICT");
  }
}

Python

class AppError(Exception):
    def __init__(self, message: str, status_code: int, code: str):
        super().__init__(message)
        self.status_code = status_code
        self.code = code

class NotFoundError(AppError):
    def __init__(self, resource: str, id: str | None = None):
        msg = f"{resource} '{id}' not found" if id else f"{resource} not found"
        super().__init__(msg, 404, "NOT_FOUND")

class ValidationError(AppError):
    def __init__(self, message: str, fields: dict | None = None):
        super().__init__(message, 400, "VALIDATION_ERROR")
        self.fields = fields or {}

class UnauthorizedError(AppError):
    def __init__(self, message: str = "Authentication required"):
        super().__init__(message, 401, "UNAUTHORIZED")

class ForbiddenError(AppError):
    def __init__(self, message: str = "Access denied"):
        super().__init__(message, 403, "FORBIDDEN")

Consistent error response shape

Every error response from the API must have the same shape so clients can handle errors generically:

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User '42' not found",
    "fields": { "email": "Email already in use" }
  }
}
  • code — machine-readable, stable across releases. Clients switch on this.
  • message — human-readable, safe to display. No stack traces, no SQL, no file paths.
  • fields — optional, for validation errors that map to specific inputs.

Never return 500 Internal Server Error with the actual exception message.


Global error handler

Express (TypeScript)

import { Request, Response, NextFunction } from "express";
import { AppError } from "./errors";

// Must have 4 parameters — Express identifies error handlers by arity
export function globalErrorHandler(
  err: Error,
  req: Request,
  res: Response,
  _next: NextFunction
) {
  // Log everything server-side
  req.log?.error({ err }, "Request failed") ?? console.error(err);

  if (err instanceof AppError && err.isOperational) {
    return res.status(err.statusCode).json({
      error: {
        code: err.code,
        message: err.message,
        ...(err instanceof ValidationError && { fields: err.fields }),
      },
    });
  }

  // Programmer error or unknown — hide internals from client
  return res.status(500).json({
    error: {
      code: "INTERNAL_ERROR",
      message: "An unexpected error occurred.",
    },
  });
}

// Register last, after all routes
app.use(globalErrorHandler);

FastAPI (Python)

from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
    log.error("request.failed", path=request.url.path, error=str(exc), exc_info=True)
    body = {"error": {"code": exc.code, "message": str(exc)}}
    if isinstance(exc, ValidationError) and exc.fields:
        body["error"]["fields"] = exc.fields
    return JSONResponse(status_code=exc.status_code, content=body)

@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
    log.error("request.unhandled_error", path=request.url.path, exc_info=True)
    return JSONResponse(
        status_code=500,
        content={"error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred."}},
    )

Async error handling

In Node.js, unhandled promise rejections crash the process (Node 15+) or produce silent failures (older). Wrap async route handlers:

// Utility: wraps an async handler so Express catches rejections
export const asyncHandler =
  (fn: (req: Request, res: Response, next: NextFunction) => Promise<void>) =>
  (req: Request, res: Response, next: NextFunction) =>
    fn(req, res, next).catch(next);  // forwards to the global error handler

// Usage
router.get("/users/:id", asyncHandler(async (req, res) => {
  const user = await userService.findById(req.params.id);
  if (!user) throw new NotFoundError("User", req.params.id);
  res.json(user);
}));

Unhandled rejection safety net

// In your process entrypoint — catches anything that slips through
process.on("unhandledRejection", (reason) => {
  console.error("Unhandled rejection:", reason);
  // For programmer errors, crash and let the process manager restart
  process.exit(1);
});

process.on("uncaughtException", (err) => {
  console.error("Uncaught exception:", err);
  process.exit(1);
});

Where to throw vs where to catch

LayerThrowCatch
Route handlerNotFoundError, ValidationErrorNothing — let the global handler catch
Service layerConflictError, ForbiddenErrorExternal service errors (wrap and re-throw)
Repository / DB layerWrap DB errors into AppError subclassesRaw DB driver errors
Global handler—Everything

Catch at a layer only if you can add context or recover. Otherwise re-throw.

// Repository — translate DB errors into app errors
async function createUser(data: CreateUserInput) {
  try {
    return await db.user.create({ data });
  } catch (err: any) {
    if (err.code === "P2002") {  // Prisma unique constraint violation
      throw new ConflictError("A user with this email already exists");
    }
    throw err;  // unknown error — let it bubble
  }
}

What not to do

// ❌ Swallowing — the error disappears silently
try {
  await sendEmail(user.email);
} catch {}

// ❌ Logging without re-throwing — the request succeeds when it shouldn't
try {
  await chargeCard(amount);
} catch (err) {
  console.error(err);
  // execution continues — order marked as paid when charge failed
}

// ❌ Returning the raw error message to the client
res.status(500).json({ error: err.message });  // may expose SQL, file paths, internals

// ❌ Using a generic Error when a specific class exists
throw new Error("not found");  // handler can't distinguish from a programmer error

// ✅ Correct: throw specific, catch at the boundary, log server-side, return safe shape
throw new NotFoundError("User", userId);

Checklist

  • Custom error class hierarchy defined (AppError base + specific subclasses)
  • Global error handler registered as the last middleware / exception handler
  • All error responses use a consistent JSON shape (code, message, fields)
  • Stack traces and internal messages never sent to the client
  • Async route handlers wrapped or using framework-native async support
  • unhandledRejection and uncaughtException handlers in the process entrypoint
  • DB / external service errors caught at the repository layer and wrapped into AppError
  • No bare catch {} or except: pass blocks — every catch logs or re-throws
  • Operational errors (4xx) logged at WARN; programmer errors (5xx) logged at ERROR
  • Error response for 500 returns "An unexpected error occurred.", not the real message

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.