When API routes return { error: string } in some places, { message: string } in others, and { success: false, reason: string } elsewhere, frontend error handling code must branch on shape — and branches that aren't tested get skipped. CWE-755 (Improper Handling of Exceptional Conditions) applies when inconsistency leads to unhandled error shapes in client code. ISO 25010 maintainability.consistency classifies format divergence as a direct maintainability defect. Practically, inconsistent formats cause error messages to silently go undisplayed: a client written against { error } silently ignores a response shaped as { message }, and users see no error feedback at all.
High because inconsistent error shapes cause client-side error handling branches to silently fail, producing invisible errors and confusing UX at the exact moments users need clarity.
Create a single error response helper in src/lib/api-response.ts and use it in every route's catch block.
// src/lib/api-response.ts
export function errorResponse(message: string, status: number = 500) {
return NextResponse.json({ error: message }, { status })
}
export function validationErrorResponse(errors: Record<string, string>) {
return NextResponse.json({ error: 'Validation failed', fields: errors }, { status: 422 })
}
Standard status codes to apply: 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity, 429 Too Many Requests, 500 Internal Server Error. Audit existing routes for any that return 200 for error conditions — these must be corrected to 4xx or 5xx.
ID: saas-error-handling.user-errors.api-consistent-error-format
Severity: high
What to look for: Examine all API route error responses across the project. Look for consistency in the shape of error JSON responses. Check for: a common envelope (e.g., always { error: string }, always { message: string }, always { error: { code: string, message: string } }, or always tRPC's standard error shape). Look for inconsistencies: some routes returning { error: '...' }, others returning { message: '...' }, others returning plain strings, others returning { success: false, reason: '...' }. Also check HTTP status codes — are 4xx codes used for client errors and 5xx for server errors, or is everything returning 200? Server actions: If the project uses Next.js server actions instead of API routes for mutations, check whether server actions return a consistent shape for error states (e.g., always { error: string } | { success: true }) or whether each action returns different shapes. Server actions that throw rather than return errors are also inconsistent — they produce Next.js digest errors in production which are opaque to callers.
Pass criteria: Enumerate all API error response shapes across routes. Pass if at least 90% of API routes return error responses in the same JSON shape, and HTTP status codes correctly distinguish client errors (4xx) from server errors (5xx). A centralized error response helper function is a strong positive signal. Report the count: "X of Y API routes use a consistent error response shape."
Fail criteria: Fail if error response shapes vary significantly across routes (2 or more distinct shapes in active use). Fail if 4xx and 5xx errors are not distinguished (everything returns 200 with an error flag). Do not pass when a centralized helper exists but some routes bypass it with ad-hoc error responses.
Skip (N/A) when: The project has no API routes and no server actions. Signal: no app/api/ directory, no pages/api/ directory, no serverless functions, no 'use server' directives.
Detail on fail: "3 distinct error shapes found: {error: string} in auth routes, {message: string} in user routes, {success: false} in payment routes; some routes return 200 for all errors". Max 500 chars.
Remediation: Inconsistent error formats force frontend code to handle multiple shapes, leading to missed error states and confusing UX. Clients can't build reliable error handling when the shape is unpredictable.
Create a single error response helper and use it everywhere:
// lib/api-response.ts
export function errorResponse(message: string, status: number = 500) {
return NextResponse.json({ error: message }, { status })
}
export function validationErrorResponse(errors: Record<string, string>) {
return NextResponse.json({ error: 'Validation failed', fields: errors }, { status: 422 })
}
Then in every route:
} catch (error) {
return errorResponse('Internal server error', 500)
}
Standard HTTP status codes to follow: 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity, 429 Too Many Requests, 500 Internal Server Error.