Error Handling
UniPulse implements a consistent error handling strategy across the entire API. All errors are caught by the global error handler and returned in a structured JSON format.
Error Response Format
Every API error follows this structure:
{
"error": {
"code": "NOT_FOUND",
"message": "Post not found",
"details": {}
}
}
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code (see table below) |
message | string | Human-readable error description |
details | object | Additional context (validation errors, field-specific info) |
Error Codes
| Code | HTTP Status | Description | Common Cause |
|---|---|---|---|
VALIDATION_ERROR | 400 | Request body failed Zod validation | Missing/invalid fields |
BAD_REQUEST | 400 | General bad request | Malformed input |
UNAUTHORIZED | 401 | Missing or invalid JWT | Expired token, missing header |
FORBIDDEN | 403 | Insufficient role or permissions | Wrong RBAC role, feature not in plan |
NOT_FOUND | 404 | Resource not found | Invalid ID, deleted resource |
CONFLICT | 409 | Duplicate resource | Unique constraint violation |
QUOTA_EXCEEDED | 429 | Workspace usage quota exceeded | Plan limit reached |
RATE_LIMITED | 429 | Too many requests | IP or workspace rate limit |
INTERNAL_ERROR | 500 | Unexpected server error | Unhandled exception |
AppError Class
Services throw typed errors using the AppError class:
// Definition
class AppError extends Error {
constructor(
public code: string,
public message: string,
public statusCode: number = 400,
public details?: Record<string, unknown>
) {
super(message);
}
}
// Usage in services
throw new AppError('NOT_FOUND', 'Post not found', 404);
throw new AppError('FORBIDDEN', 'Insufficient permissions', 403);
throw new AppError('CONFLICT', 'Workspace name already exists', 409);
throw new AppError('QUOTA_EXCEEDED', 'Monthly AI generation limit reached', 429, {
current: 500,
limit: 500,
resetDate: '2026-04-01',
});
Global Error Handler (errorHandler.ts)
The global error handler is registered as the last Express middleware. It catches all errors that propagate from route handlers and services.
// Registered in index.ts
app.use(errorHandler);
Error Type Handling
| Error Type | Handling | Response Code |
|---|---|---|
AppError | Maps directly to HTTP status and error code | From AppError.statusCode |
ZodError | Extracts field-level validation errors | 400 VALIDATION_ERROR |
Prisma P2002 | Unique constraint violation | 409 CONFLICT |
Prisma P2025 | Record not found | 404 NOT_FOUND |
Prisma P2003 | Foreign key constraint | 400 BAD_REQUEST |
Unknown Error | Logs full stack, returns generic message | 500 INTERNAL_ERROR |
Validation Error Response Example
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{
"field": "caption",
"message": "String must contain at least 1 character(s)"
},
{
"field": "platforms",
"message": "Array must contain at least 1 element(s)"
}
]
}
}
Error Handling Best Practices
In Services
// Throw AppError for expected business errors
async getById(workspaceId: string, id: string) {
const post = await prisma.post.findFirst({
where: { id, workspaceId },
});
if (!post) {
throw new AppError('NOT_FOUND', 'Post not found', 404);
}
return post;
}
In Route Handlers
// Use try-catch for async handlers, or use an asyncHandler wrapper
router.get('/:id', authenticate, requireWorkspace('VIEWER'), async (req, res, next) => {
try {
const post = await postService.getById(req.workspace.id, req.params.id);
res.json(post);
} catch (error) {
next(error); // Passes to global error handler
}
});
In Queue Workers
// Queue workers handle errors via BullMQ retry mechanism
const worker = new Worker('publish', async (job) => {
try {
await postService.publish(job.data.postId, job.data.platforms);
} catch (error) {
// BullMQ will retry based on job options
throw error;
}
});
Never Swallow Errors
Always let errors propagate to the global handler or queue retry mechanism. Do not catch errors silently -- this makes debugging extremely difficult. If you need to handle a specific error case, re-throw a more specific AppError.
Error Logging
| Environment | Logging Level | Details |
|---|---|---|
development | Full stack traces | Console output |
production | Error code + message | Structured JSON logs, no stack traces in responses |
All 5xx errors are logged with full context (request path, user ID, workspace ID, request body) for debugging.
Cross-Reference
- Middleware -- errorHandler.ts in the middleware chain
- API Reference -- standard error format
- Shared Validators -- Zod schemas that drive VALIDATION_ERROR