Global Error Handling
Your approach to handling global error handling. Use this skill when working on files where global error handling comes into play.
SKILL.md
Full skill instructions
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", "[email protected]");
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
- Always use asyncHandler/asyncAuthHandler for async route handlers
- Always throw specific error classes instead of generic Error
- Always use factory methods when available (e.g.,
NotFoundError.contact()) - Never manually send error responses - let the global handler do it
- Pass errors to next() if you catch them locally
- Use logger for debugging, not console.log
- 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
