Generic outward, detailed inward
The caller gets a reference they can quote to support. You get everything, tied to that reference.
What must not appear in a response: stack traces, file paths, database error text, framework versions, SQL fragments, internal hostnames, other users' data in a validation message.
Debug mode is the big one
Most of the serious cases aren't handwritten error handlers, they're a framework's debug page left enabled.
Laravel's Ignition page shows environment variables including database credentials. Django's debug page shows settings and query history. Rails shows source and locals. Flask's Werkzeug debugger offers an interactive console, which is remote code execution.
APP_DEBUG=false, DEBUG = False, config.consider_all_requests_local = false, debug=False. Check the deployed configuration rather than the file in the repository.
Enumeration through differences
Errors leak by varying, not only by being verbose.
Same response for both. Same status code, same body, similar timing. And on login, verify a dummy hash when the user doesn't exist, or the response time answers the question your message didn't.
Same principle on password reset ("if an account exists, we've sent an email"), on registration, and on any lookup by an identifier.
404 rather than 403
When someone requests a record they don't own, 403 confirms it exists. 404 doesn't.
One query, one response, no distinction between "doesn't exist" and "isn't yours".
Validation errors
The one place detail is genuinely useful, "email is required" helps and reveals nothing. Keep it to the shape of the input, not the state of the data. "That email is already registered" is an enumeration oracle wearing a helpful hat.
Client-side too
Source maps in production expose your original source. Verbose console logging exposes internal structure. Error tracking tools capture request bodies by default, which is how tokens end up in a third-party service. Configure scrubbing.
Testing it
Trigger errors deliberately in staging: malformed JSON, a bad database credential, a missing record, an unauthorised access. Read the responses. This is quick and it's the only way to know what your stack actually returns rather than what you configured.