Awesome-omni-skill Global Error Handling

Your approach to handling global error handling. Use this skill when working on files where global error handling comes into play.

install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/global-error-handling" ~/.claude/skills/diegosouzapw-awesome-omni-skill-global-error-handling && rm -rf "$T"
manifest: skills/development/global-error-handling/SKILL.md
source content

Global Error Handling

This Skill provides Claude Code with specific guidance on how to handle errors consistently across the MorrisB application.

CRITICAL: Always Follow This Pattern

When writing or modifying any backend route, service, or middleware, you MUST use the custom error handling system.

Error Classes Available

Import errors from

../errors
(adjust path as needed):

import {
    // Error classes
    AppError,
    ValidationError,
    UnauthorizedError,
    ForbiddenError,
    NotFoundError,
    ConflictError,
    RateLimitError,
    InternalError,
    ExternalServiceError,

    // Async handler wrapper
    asyncHandler,
    asyncAuthHandler,
} from "../errors";

Route Handler Pattern (REQUIRED)

Option 1: asyncHandler Wrapper (Preferred)

Use

asyncHandler
or
asyncAuthHandler
to automatically catch errors:

import { asyncAuthHandler, NotFoundError, ForbiddenError } from "../errors";

router.get("/:workspaceId/contacts/:id", authenticate, asyncAuthHandler(async (req, res) => {
    const { workspaceId, id } = req.params;
    const userId = req.user?._id?.toString();

    // Validate workspace access - throws ForbiddenError
    const workspace = await Project.findById(workspaceId);
    if (!workspace) {
        throw NotFoundError.workspace(workspaceId);
    }
    if (workspace.userId.toString() !== userId) {
        throw ForbiddenError.workspaceAccess();
    }

    // Find contact - throws NotFoundError
    const contact = await Contact.findOne({ _id: id, workspaceId });
    if (!contact) {
        throw NotFoundError.contact(id);
    }

    res.json({ success: true, data: { contact } });
}));

Option 2: Traditional try/catch (Use sparingly)

Only use try/catch when you need specific error handling:

router.post("/register", async (req, res, next) => {
    try {
        const validatedData = registerSchema.parse(req.body);
        // ... logic
    } catch (error: any) {
        if (error.name === "ZodError") {
            return next(ValidationError.fromZod(error));
        }
        next(error); // Pass to global error handler
    }
});

Error Factory Methods

Use static factory methods for common error scenarios:

ValidationError

throw ValidationError.missingField("email");
throw ValidationError.invalidFormat("phone", "+1-XXX-XXX-XXXX");
throw ValidationError.invalidId("contact");
throw ValidationError.fromZod(zodError);

UnauthorizedError

throw UnauthorizedError.noToken();
throw UnauthorizedError.invalidToken();
throw UnauthorizedError.tokenExpired();
throw UnauthorizedError.invalidCredentials();

ForbiddenError

throw ForbiddenError.emailNotVerified();
throw ForbiddenError.workspaceAccess();
throw ForbiddenError.insufficientPermissions("delete contacts");

NotFoundError

throw NotFoundError.workspace(workspaceId);
throw NotFoundError.contact(contactId);
throw NotFoundError.user(userId);
throw NotFoundError.campaign(campaignId);
throw NotFoundError.sequence(sequenceId);
throw new NotFoundError("Custom Resource", identifier);

ConflictError

throw ConflictError.emailExists();
throw ConflictError.duplicate("Contact", "email", "user@example.com");

ExternalServiceError

throw ExternalServiceError.email("Failed to send verification email");
throw ExternalServiceError.ai("OpenAI rate limit exceeded");

DO NOT DO THIS

Never manually send error responses in routes:

// BAD - Don't do this
res.status(404).json({
    success: false,
    error: "Contact not found",
});

Never catch and swallow errors silently:

// BAD - Don't do this
try {
    await someOperation();
} catch (error) {
    console.error(error); // Lost error!
}

Never use generic error messages:

// BAD - Not helpful
throw new Error("Something went wrong");

DO THIS INSTEAD

Throw specific error classes:

// GOOD
throw NotFoundError.contact(contactId);

Use asyncHandler to auto-catch:

// GOOD - Errors automatically passed to global handler
router.get("/", asyncHandler(async (req, res) => {
    // If this throws, global handler catches it
    const data = await getData();
    res.json({ success: true, data });
}));

Pass errors to next() when in try/catch:

// GOOD - Passes to global error handler
} catch (error) {
    next(error);
}

Server Setup Required

The global error handler must be registered in

server.ts
:

import { globalErrorHandler, notFoundHandler } from "./middleware/errorHandler";

// After all routes
app.use(notFoundHandler);
app.use(globalErrorHandler);

Error Response Format

All errors are returned in this consistent format:

{
    "success": false,
    "error": {
        "message": "Contact not found",
        "code": "RES_001",
        "details": {
            "resource": "Contact",
            "identifier": "123abc"
        }
    }
}

Best Practices Summary

  1. Always use asyncHandler/asyncAuthHandler for async route handlers
  2. Always throw specific error classes instead of generic Error
  3. Always use factory methods when available (e.g.,
    NotFoundError.contact()
    )
  4. Never manually send error responses - let the global handler do it
  5. Pass errors to next() if you catch them locally
  6. Use logger for debugging, not console.log
  7. Include helpful details in error constructors for debugging

File References

  • Error classes:
    backend/src/errors/AppError.ts
  • Error handler middleware:
    backend/src/middleware/errorHandler.ts
  • Index exports:
    backend/src/errors/index.ts