| name | Global Error Handling |
| description | Your approach to handling global error handling. Use this skill when working on files where global error handling comes into play. |
| globs | ["backend/src/routes/**/*.ts","backend/src/middleware/**/*.ts","backend/src/services/**/*.ts","backend/src/controllers/**/*.ts"] |
| alwaysApply | true |
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 {
AppError,
ValidationError,
UnauthorizedError,
ForbiddenError,
NotFoundError,
ConflictError,
RateLimitError,
InternalError,
ExternalServiceError,
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();
const workspace = await Project.findById(workspaceId);
if (!workspace) {
throw NotFoundError.workspace(workspaceId);
}
if (workspace.userId.toString() !== userId) {
throw ForbiddenError.workspaceAccess();
}
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);
} catch (error: any) {
if (error.name === "ZodError") {
return next(ValidationError.fromZod(error));
}
next(error);
}
});
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:
res.status(404).json({
success: false,
error: "Contact not found",
});
❌ Never catch and swallow errors silently:
try {
await someOperation();
} catch (error) {
console.error(error);
}
❌ Never use generic error messages:
throw new Error("Something went wrong");
DO THIS INSTEAD
✅ Throw specific error classes:
throw NotFoundError.contact(contactId);
✅ Use asyncHandler to auto-catch:
router.get("/", asyncHandler(async (req, res) => {
const data = await getData();
res.json({ success: true, data });
}));
✅ Pass errors to next() when in try/catch:
} catch (error) {
next(error);
}
Server Setup Required
The global error handler must be registered in server.ts:
import { globalErrorHandler, notFoundHandler } from "./middleware/errorHandler";
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