Description
Backend error responses are inconsistent across routes. Some return { error: 'message' }, others return { message: 'error' }, others return raw Express errors. The SDK and client have to handle different error shapes depending on which endpoint failed.
Current State (inconsistent)
// Route A
{ "error": "Subscription not found" }
// Route B
{ "message": "Unauthorized", "status": 401 }
// Route C (unhandled Express error)
{ "message": "Internal server error" }
Solution: RFC 7807 Problem Details
{
"type": "https://syncro.app/errors/not-found",
"title": "Subscription Not Found",
"status": 404,
"detail": "No subscription with ID 'abc-123' exists for this user.",
"instance": "/api/v1/subscriptions/abc-123",
"requestId": "550e8400-e29b-41d4-a716"
}
Implementation
Error classes
// /backend/src/errors/index.ts
export class AppError extends Error {
constructor(
public title: string,
public status: number,
public detail: string,
public type: string = 'about:blank'
) { super(detail); }
}
export class NotFoundError extends AppError {
constructor(detail: string) {
super('Not Found', 404, detail, 'https://syncro.app/errors/not-found');
}
}
export class ValidationError extends AppError {
constructor(detail: string, public errors?: Record<string, string[]>) {
super('Validation Error', 422, detail, 'https://syncro.app/errors/validation');
}
}
export class UnauthorizedError extends AppError {
constructor() {
super('Unauthorized', 401, 'Authentication required.', 'https://syncro.app/errors/unauthorized');
}
}
Global error handler middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const requestId = res.getHeader('x-request-id') as string;
if (err instanceof AppError) {
return res.status(err.status).json({
type: err.type,
title: err.title,
status: err.status,
detail: err.detail,
instance: req.path,
requestId,
});
}
// Unexpected error — don't leak internals
logger.error('Unhandled error', { err, requestId });
res.status(500).json({
type: 'https://syncro.app/errors/internal',
title: 'Internal Server Error',
status: 500,
detail: 'An unexpected error occurred.',
instance: req.path,
requestId,
});
});
Acceptance Criteria
Description
Backend error responses are inconsistent across routes. Some return
{ error: 'message' }, others return{ message: 'error' }, others return raw Express errors. The SDK and client have to handle different error shapes depending on which endpoint failed.Current State (inconsistent)
Solution: RFC 7807 Problem Details
{ "type": "https://syncro.app/errors/not-found", "title": "Subscription Not Found", "status": 404, "detail": "No subscription with ID 'abc-123' exists for this user.", "instance": "/api/v1/subscriptions/abc-123", "requestId": "550e8400-e29b-41d4-a716" }Implementation
Error classes
Global error handler middleware
Acceptance Criteria
AppErrorsubclasses instead of generic errorsValidationErrorwith field-level details