Don't Just Say 'Failed': Error Messages and Empty States for Vibe Projects
Errors and empty states are where a product's character shows: say whether it worked, say why, say what is next, and protect the user's work. This guide covers error copy rules, three kinds of empty states, error placement, retry logic, and a pre-launch checklist.

How good a product feels is rarely decided when everything works. It is decided when something goes wrong. When things run smoothly, anyone can click "next." When they get stuck, users start judging: is this product reliable, and should I keep going?
This guide is about being considerate when things go wrong: how to write error messages, how to present empty states, and what way back to offer after a failure. The example product "Lighthouse Run Club" (a small app for organizing group runs) and the person "Azhou" are fictional, used to ground the ideas in concrete screens. The methods apply to any vibe project.
First, tell three things apart: errors, empty states, and load failures
People often lump these three together, and the result is confusion: a page with simply no data yet puts on a "something broke" face, while a genuinely broken connection is brushed off as "no content." Mislead a user once, and you lose a notch of trust.
An error means the user tried something and it did not work: a save failed, a payment did not go through, a form has mistakes. An empty state means the action worked but the result is zero items: a new account has no run records yet, or filters are too strict to match any event. A load failure means the result never even arrived: the network dropped, or the server did not respond. The causes differ, and so does what the user should do next — so the copy and the buttons must be written separately, never sharing one lazy line of "no data."
A quick self-test: write one sentence for each state. An error should answer "which step failed." An empty state should answer "why is it empty, and what can I do next." A load failure should answer "did we fail to connect, or did the other side not respond, and can I retry." Any state that cannot answer its question is not finished.
Four rules for error copy
First, speak human, not error codes. A user who sees "Error 5003" only panics more; what they want to know is whether the step worked and what to do. Error codes can live in developer logs, but on screen, lead with a sentence a person understands: "The save did not go through. Your changes are still here — hit retry to save again."
Second, name exactly what is wrong. The worst form error is a single vague banner at the top — "Please check your input" — that forces the user to guess field by field. The right way is to mark the faulty field right where it is: "The phone number is one digit short" or "The end time cannot be earlier than the start time." Flag only the fields that are actually wrong; do not paint the whole form red.
Third, every error gets a next step. An error with no next step leaves the user stranded. A failed save gets "Retry"; missing permission gets "Grant access" or "Contact an admin"; rejected content gets "See the guidelines" or "Edit and resend." Even "Go back" beats a bare failure message.
Fourth, do not blame the user. "Your input is invalid" sounds like an accusation. Say it neutrally instead: "This date format does not work — try 2026-10-30." Same correction, different tone, and a very different willingness to cooperate. When Azhou first built Lighthouse Run Club, a failed sign-up just said "Sign-up failed," and the user group erupted. Changing it to "This run just filled up — see the next one here" cut complaints by more than half. The failure did not change; the road after it did.
Three kinds of empty state, three ways to write them
The first is the "newcomer empty": a user just signed up and has nothing yet. This is not a report; it is an invitation. Do not just write "No records yet" — write "No runs yet — come try your first one," with a prominent main button. Artwork can be light and friendly, but avoid a dreary grey "empty folder" icon. A newcomer whose first screen looks grey is likely to just close the app.
The second is "filtered to zero": the user set conditions and nothing matched. The key message is "the system is not broken, your conditions are just too tight," plus a way to loosen them: show the current conditions and offer "Clear filters" or "Widen to the whole city — 3 more runs there." Early Lighthouse Run Club taught this lesson the hard way: filtering by "Saturday morning, within 5 km, 5-minute pace" often returned zero, and users concluded nobody used the product. Adding "Widen to 10 km and there are 3 more runs" to the empty state visibly improved retention.
The third is "emptied by the user": they cleared their own content, say all their saved events. Be gentle, confirm the result, and leave a way back: "Saved events cleared — you can re-save from the browse page." Never use this moment to push new features. Someone who just deleted things is annoyed; a sales pitch will only annoy them more.
After a failure, the user's work must survive
The most damaging failure is not the error itself, but losing the user's work because of it. Ten minutes of form filling, one failed submit, one refresh, everything gone — that user will not come back. So whatever users typed must stay put when a submit fails, and ideally survive a refresh too (keeping a local draft copy is not hard). Deletions deserve even more care: confirm first, execute second, offer undo after — and state the undo window plainly. "Undo within 30 seconds" reassures far more than a vague "can be undone."
Then there is the easily missed case: partial success. Signing up for 3 runs, 2 succeed, 1 is full. Do not report only "Sign-up failed," and do not report only "Sign-up succeeded" either — lay out both parts: "Signed up for 2 runs; 1 did not go through because it filled up." Honesty about the half that worked is worth more than any polished success message.
Do not let technical details leak onto the screen
"Network request timed out," "Database connection failed," "Parameter validation failed" — these are written for developers, not users. A rough translation rule works: turn the technical cause into something the user can perceive. "Request timed out" becomes "The connection is slow and did not get through"; "validation failed" becomes the specific field that is wrong; "server error" becomes "Something went wrong on our side — we are on it, please try again later." While translating, add the next step too: retry where retry helps, a way back or a contact where it does not.
But do not swing to the opposite extreme and call everything "a small hiccup." Three "small hiccups" in a row, and the fourth will not be believed. Be honest about size: small problems are small problems; things that take time should say how long; things that cannot wait should offer an alternative. Friendliness is not vagueness — honesty is itself a form of friendliness.
Three error placements — do not mix them up
Where an error appears matters as much as what it says. The first placement is inline, next to the field — right for forms. The error lives on the line the user is editing and clears as soon as it is fixed; do not wait until submit to dump them all at once. One restraint on live validation: do not flag errors while the user is still typing. Judging a half-typed value as "wrong format" annoys everyone; validate when they leave the field or hit submit.
The second is a light banner at the top or bottom — right for small, non-blocking failures: "Avatar upload failed, your old avatar is kept." It must not block what the user is doing, can disappear after a few seconds, but must be openable for details before it vanishes. If users want to dig deeper, there has to be somewhere to dig.
The third is a centered dialog — right for things that must stop the user: a failed payment, a failed pre-delete check, an expired session. The test for a dialog is "cannot proceed without this": once shown, the user must deal with it first, so abusing dialogs means abusing attention. Azhou once made "network wobble" a dialog in Lighthouse Run Club, and riders on the subway got popped once per station; switching it to a light banner ended the complaints. The rule is simple: if not handling it risks loss or a wrong action, use a dialog; otherwise use a light hint.
Before-and-after: rewrite the copy side by side
"Operation failed" becomes "Sign-up did not go through — the spot may just have been taken. Retry to check again." "Please enter valid characters" becomes "Nicknames can only use Chinese characters, letters and numbers — no spaces." "System error, please try again later" becomes "Something went wrong on our side and the last action did not take effect. Try again in a bit, or contact us here if it keeps happening." Cause, effect, and contact — all three present — and the user can relax.
There is one more easily missed pattern: failure right after success. Sign-up succeeded, but the SMS notification did not go out. Do not pretend everything is fine, and do not roll back the success — add one line under the success message: "Signed up. The SMS did not go out, but you can check 'My events' anytime." What users fear most is not a problem, but not knowing whether it worked. Saying plainly whether it worked always comes first.
Retry is not a cure-all — split it by case
Before adding a retry button, ask: will retrying help? For network jitter, retry probably helps, so put the button front and center. But for a full event, missing permission, or rejected content, retrying a hundred times helps nothing — the button should become "See other events," "Request access," or "Edit and resend." Worse still is silent infinite auto-retry: the page spins forever and the user cannot tell retry from a freeze. Cap auto-retries at two or three attempts, keep the progress visible, and after that stop, say plainly that it failed, and hand the choice back to the user.
Retries also need to protect user data. The classic trap: hitting retry submits the same sign-up twice. The fix is not complicated — disable the submit button after the first tap until a result comes back, or give each submission a unique id so the backend returns the first result for duplicates. That sounds like backend detail, but what the user experiences is "will I be charged twice, signed up twice." Whatever backend you use, test "what happens if I tap submit twice fast" before launch. It is the most worthwhile minute of testing there is.
Artwork and buttons in empty states should not steal the show
Empty states are often designed as "the bigger the illustration, the better," and the user ends up seeing a picture instead of what to do next. The right order is: one sentence saying what is going on, one main button for the action, illustration as atmosphere — small, to the side or on top. Button copy matters too: "Go have a look" is vague; "Book your first run" is not. The more concrete the verb on the button, the clearer what tapping it will do.
One more detail: do not mix empty states with ad slots. A newcomer opening "My saved events" for the first time should see "Nothing saved yet — browse events," not a wall of recommendations. Recommendations are fine below the main action, labeled "Recommended for you" — never let users confuse a status message with a sales pitch. The first duty of a status page is to report the situation honestly; promotion always comes second.
Write failures into the test list, not into apology letters
Many teams only fix error copy after complaints arrive. The cheaper habit is to add one line to every feature spec: "How can this fail? What will the user see? What is the next step? Will data be lost?" Fill that line in while building the feature, trigger each failure once before launch, and complaints drop sharply. Azhou later made it a habit: for each new feature, write the failure list before the success flow. Reversing that order cut rework substantially.
One last reminder: error copy needs to evolve with the product. "This run just filled up" is enough early on; later you may need "3 more runs nearby are open"; at scale you may need "You are queued — we will notify you when a spot opens." Re-read old error copy on every release and retire what is out of date. An expired reassurance disappoints more than no reassurance at all.
Do not forget the people who cannot see the error
Errors are not only for eyes. Screen-reader users cannot hear red text or see icon colors, so errors must be readable: placed where assistive tech can find them, and tied to the field itself rather than floating in a corner. Color alone is also a trap — red-green color-blind users cannot tell right from wrong by color, so errors need an icon or words as well as red. Keyboard users matter too: when an error appears, focus should move to the first faulty field or the message, not leave users hunting across the page.
Motion deserves restraint as well. A full-screen shake or flashing red text on error looks "attention-grabbing" but is genuinely uncomfortable for motion-sensitive users. One gentle hint, sitting still, is more professional than flashy shaking. Consideration is not piling on effects; it is making sure every user can learn, in their own way and at the first moment, what happened and what to do next.
Look back at the whole guide and it boils down to one sentence: when things fail, do not make users guess. Say whether it worked, say why, say what is next, and protect their work. Do those four things, and your product is most trustworthy precisely at its hardest moment.
Comments (0)
Related articles

Waiting hurts mostly because of uncertainty, not time. This guide covers skeleton screens vs spinners vs progress bars, the missing 'submitting' button state, background processing, optimistic updates, upload progress, and timeout handling.

When a new user leaves three minutes after signup, registration is rarely the problem; the first task after it failed. This guide gives vibe-coded projects a first-success method: define one specific, observable first success, pick among sample data, a real task, or import, then handle interruptions, failures, and the single next step.

When a user hits zero results and leaves, the item often exists; they just cannot tell which condition to change. This guide gives vibe-coded projects a search-and-filter method: keyword-filter division of labor, OR-within-group and AND-across-groups logic, counts on options, shareable URL state, and a four-step empty-result recovery.