Why users don't see my error message — 3 causes and how to check each

The symptom is always the same: someone writes "I pressed the button and nothing happened." You open the code — the error message is there. You test it yourself — visible. You watch a session recording — the person taps the button eight times and leaves.
The error did render. It just rendered in the wrong place, at the wrong time, or in a way nobody registers. Three causes, ordered by frequency — it's almost always one of them.
Cause 1: the message is outside what's on screen
The most common one, and the hardest to notice while developing.
The form is long. The Email field is at the top, the Submit button at the bottom. They press it, the error appears next to the email field — two screens above. The page didn't move. From their point of view, nothing happened at all.
You never reproduce it, because on your large monitor the whole form fits.
How to check. Open the form, squash the browser window vertically to phone height, scroll to the button and submit it empty. If the viewport didn't move, that's your cause.
How to fix. After a failed submit, scroll to the first error and move focus there:
const firstInvalid = form.querySelector('[aria-invalid="true"]');
firstInvalid?.focus(); // focus scrolls to the element itself
firstInvalid?.scrollIntoView({ block: 'center', behavior: 'smooth' });
Focus specifically, not just scrolling: it solves the keyboard case and the screen reader case at once. And put it on the field, not on the error text — the person needs to start typing immediately.
Cause 2: the error went to the console, not the interface
Second most common, and it almost always comes from code written in a hurry:
try {
await api.save(data);
} catch (e) {
console.error(e); // ← the entire error UI
}
For you the error "exists": it's in the console, you see it while debugging. For the user it doesn't exist — they never open the console.
How to check. Find every catch in the project and look inside. Only a console.error, or empty braces, means a silent loss:
grep -rn "catch" src/ | grep -v "setError"
How to fix. The rule: any catch handling a user action must change interface state. Not "log it" — change it: clear the spinner, show text, put the button back to a usable state.
A variant of the same problem is when the request didn't fail at all but returned something unexpected: the server answered 200 with an empty body. Then there's no error in the console or in the UI — the classic undefined response. Check not only catch, but what actually came back.
Cause 3: the message appeared and left
The third case is showing the error as a three-second toast.
The person was looking at the button, and the panel popped up in the top-right corner. Or they were looking in the right place but read more slowly. Or it's a phone, the panel covered the button from below, they tapped again and dismissed it themselves.
The same bucket holds the case where the message is visible but unavailable to screen reader users. A block simply inserted into the markup won't be announced: for that you need a live region that already sits in the markup, empty, with the text appearing inside it:
<div id="form-status" role="status"></div>
How to check. Ask the person who complained one question: "did anything flash?" An answer of "something was there, I didn't have time to read it" is a precise diagnosis.
How to fix. Never show form errors as toasts — they belong by the field. The criteria-by-criteria breakdown is in toast or inline error. If the error is global (network down, server 500), keep the text next to the button where it stays until the next attempt, and take the timer off the panel.
When it's visible and still not understood
There's a fourth layer: the message is on screen, the person read it — and still doesn't know what to do.
Three usual suspects:
- Colour instead of text. A red border with no caption is read by neither a screen reader nor someone who doesn't distinguish red.
- Too vague. "Validation error" doesn't say which field or what's wrong with it. You need both — which field, what's wrong, what it should be.
- Raw technical text.
Unexpected token < in JSON at position 0is a message for you, not for a user. What it means is covered in how to read an error message; show people a human sentence and keep the technical one in the logs.
The one-minute check
Squash the browser window to phone height. Submit an empty form. Without scrolling, look: did the viewport move to the error? Is the text still there ten seconds later? Is it obvious which field to fix?
Three yeses and you're fine. Any no is your cause.
Could it be CSS, with the block simply invisible?
It happens: the element is in the markup but hidden by styles or covered by another block. Ten seconds in the inspector answers it — and if so, that's a CSS story instead.
Should I show an error code to users?
A short identifier helps if you run support: they quote it, you find the log entry. But the identifier comes after the human explanation, never instead of it.
How do I catch this before anyone complains?
Track repeated clicks on the same button within a short window in your analytics. Three taps in five seconds nearly always means the person got no feedback.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.





