Skip to main content

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": {}
}
}
FieldTypeDescription
codestringMachine-readable error code (see table below)
messagestringHuman-readable error description
detailsobjectAdditional context (validation errors, field-specific info)

Error Codes

CodeHTTP StatusDescriptionCommon Cause
VALIDATION_ERROR400Request body failed Zod validationMissing/invalid fields
BAD_REQUEST400General bad requestMalformed input
UNAUTHORIZED401Missing or invalid JWTExpired token, missing header
FORBIDDEN403Insufficient role or permissionsWrong RBAC role, feature not in plan
NOT_FOUND404Resource not foundInvalid ID, deleted resource
CONFLICT409Duplicate resourceUnique constraint violation
QUOTA_EXCEEDED429Workspace usage quota exceededPlan limit reached
RATE_LIMITED429Too many requestsIP or workspace rate limit
INTERNAL_ERROR500Unexpected server errorUnhandled 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 TypeHandlingResponse Code
AppErrorMaps directly to HTTP status and error codeFrom AppError.statusCode
ZodErrorExtracts field-level validation errors400 VALIDATION_ERROR
Prisma P2002Unique constraint violation409 CONFLICT
Prisma P2025Record not found404 NOT_FOUND
Prisma P2003Foreign key constraint400 BAD_REQUEST
Unknown ErrorLogs full stack, returns generic message500 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

EnvironmentLogging LevelDetails
developmentFull stack tracesConsole output
productionError code + messageStructured 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