“Something went wrong” is honest in the narrowest possible sense.
Something did go wrong. The message simply refuses to say who can act, what they should do, or whether trying again will help.
These messages usually arrive at the end of a feature. The successful path has been designed and built. A failure needs a string before the ticket can close, so the application apologizes vaguely and moves on.
The cost arrives later. A user opens a support ticket with no useful detail. Support cannot tell whether the problem is input, permission, or an unavailable service. Engineering tries to reproduce a condition that the system understood at the moment it occurred and then discarded.
An error message is not finishing copy. It is part of the behaviour of the product.
A useful error routes the problem
Every failure creates a next action, even if the action belongs to the system rather than the user.
The user may need to correct a value, choose another payment method, wait, or contact support. The client application may need to stop retrying. An engineer may need to find the request in logs and inspect a failed dependency.
A good error keeps those audiences separate while connecting their view of the same event.
The user should receive plain language and an appropriate next step. They do not need an exception class or database message. The engineer should receive the technical context in structured logs, along with an identifier that can be connected to what the user saw. The calling application should receive a stable machine-readable code and an honest status.
One failure can support all three without exposing internals on the screen.
The important design question is: who can do something useful now?
Not every failure means “try again”
Generic messages flatten different conditions into one. Users then make the most available decision, which is often to repeat the action.
That is harmless for some failures and damaging for others.
If the user entered an invalid value, retrying unchanged will never work. The product should identify the field and explain what a valid value looks like.
If a dependency is temporarily unavailable and the action is safe to retry, the product should say that the problem is temporary. If the system does not know whether an order or payment completed, it should not confidently invite another attempt.
If the application encountered an unexpected defect, the user may have nothing useful to do. In that case, a reference number and a truthful statement are better than instructions that shift responsibility back to them.
I find it useful to distinguish three broad categories:
- You can correct this. Explain what needs to change.
- We are unable to complete this. Preserve the failure, provide a reference, and do not pretend the user can fix it.
- This may succeed later. Say when retrying is reasonable and make sure the operation can actually tolerate a retry.
The words can vary. The ownership should not.
The system usually knew more
Vague errors are frustrating because they are often created by throwing information away.
A broad catch block takes several specific failures and turns them into the same response. A log entry says “request failed” without naming the dependency, account, operation, or request. A server returns the same status for invalid input and an internal outage.
That simplicity exists only in the code that handles the error. Everywhere else, it creates work.
I prefer failures to keep their identity for as long as possible. Expected conditions should have explicit types or codes. Logs should capture the operation and the identifiers needed to follow it, without recording secrets or unnecessary personal data. The response should preserve enough meaning for the caller to behave correctly.
A request or correlation ID is especially useful. It gives support a piece of information they can ask for and gives engineering a direct route into the technical record. It turns “the screen failed sometime this morning” into a specific event.
That does not require showing users the machinery. “We could not reach the shipping service. Your order has not been submitted. Try again shortly. Reference: ABC123” is both understandable and operationally useful.
Error design has limits
More detail is not always better.
Authentication and authorization failures may need deliberately similar responses so the application does not reveal which accounts or records exist. Internal exception messages can expose implementation details. A user-facing explanation that is too technical can be as useless as a generic one.
The goal is not transparency at any cost. It is enough truthful information for the next safe action.
There is also a point where better copy cannot repair unreliable behaviour. If an operation has no clear transaction boundary, cannot be retried safely, or loses failures between services, the message is only describing an architectural problem. The product needs both a clear explanation and a system that can support what the explanation promises.
I now write the failure path with the feature
For work that matters, I want the likely failures named before the final catch block appears.
What can the user correct? Which dependencies can be unavailable? Can the operation be repeated? What will support need to identify the attempt? Where will the technical detail live?
Those questions influence the design, not only the words.
The happy path shows what the product can do. The failure path shows whether the product respects the user’s time and whether the team can operate what it shipped.
“Something went wrong” may be accurate.
The system owes everyone involved a more useful answer than that.