The bug
responses.error(res, status, title, description) takes the title as a caller-supplied string. Every controller therefore hand-writes it, and the overwhelmingly common shape is a dynamic status paired with a hardcoded title:
responses.error(res, err.status || 422, 'Unprocessable Entity', errors.getMessage(err))(err);
When err.status is anything other than 422, the serialised envelope contradicts itself:
{"type":"error","message":"Unprocessable Entity","code":409,"status":409,"errorCode":"CONFLICT"}
message says 422's title while status/code say 409. A 404 thrown by a service reaches the client labelled "Unprocessable Entity".
Why it belongs in the stack
The title is a pure function of the status — there is nothing project-specific about mapping 409 to "Conflict". Node exposes the mapping already:
import { STATUS_CODES } from 'node:http';
STATUS_CODES[409] // 'Conflict'
Because the title is caller-supplied, every downstream project repeats the same hand-rolled ternary in every controller, and each one drifts independently. Fixing it once in the seam fixes it everywhere and lets the duplicated ternaries be deleted.
Suggested change
Make the title optional in lib/helpers/responses.js#error and derive it from the status via STATUS_CODES when omitted. Keep the explicit-title overload working so existing call sites are untouched — this stays backward-compatible and downstream projects can drop their local ternaries at their own pace.
Regression guard: assert that for a sample of statuses (400 / 403 / 404 / 409 / 422 / 503) the serialised message matches the status.
Deliberately NOT proposed
The second half of this defect class — preferring an AppError's details.message over the generic outer description — is call-site specific and should NOT become a blanket stack default. Some controllers intentionally keep a static, safe description and must not start leaking an internal message to the client. If that behaviour is ever added here, it belongs behind an explicit opt-in flag, never on by default.
The bug
responses.error(res, status, title, description)takes the title as a caller-supplied string. Every controller therefore hand-writes it, and the overwhelmingly common shape is a dynamic status paired with a hardcoded title:When
err.statusis anything other than 422, the serialised envelope contradicts itself:{"type":"error","message":"Unprocessable Entity","code":409,"status":409,"errorCode":"CONFLICT"}messagesays 422's title whilestatus/codesay 409. A 404 thrown by a service reaches the client labelled "Unprocessable Entity".Why it belongs in the stack
The title is a pure function of the status — there is nothing project-specific about mapping 409 to "Conflict". Node exposes the mapping already:
Because the title is caller-supplied, every downstream project repeats the same hand-rolled ternary in every controller, and each one drifts independently. Fixing it once in the seam fixes it everywhere and lets the duplicated ternaries be deleted.
Suggested change
Make the title optional in
lib/helpers/responses.js#errorand derive it from the status viaSTATUS_CODESwhen omitted. Keep the explicit-title overload working so existing call sites are untouched — this stays backward-compatible and downstream projects can drop their local ternaries at their own pace.Regression guard: assert that for a sample of statuses (400 / 403 / 404 / 409 / 422 / 503) the serialised
messagematches the status.Deliberately NOT proposed
The second half of this defect class — preferring an
AppError'sdetails.messageover the generic outer description — is call-site specific and should NOT become a blanket stack default. Some controllers intentionally keep a static, safe description and must not start leaking an internal message to the client. If that behaviour is ever added here, it belongs behind an explicit opt-in flag, never on by default.