Watch someone fill in a long form with a screen reader and you'll see the same thing happen again and again. They press submit, nothing seems to happen, and a red border has appeared on a field three screens up. Sighted users with a mouse find it eventually. Everyone else is stuck.
Good error handling is not hard, but it has to cover three separate things.
1. Say which field is wrong, in text
WCAG 3.3.1 Error Identification (Level A): "If an input error is automatically detected, the item that is in error is identified and the error is described to the user in text."
Two words matter: identified and text. A red border identifies the field only to people who can see colour. The fix is a message next to the field, tied to it in code so screen readers read it with the field:
<label for="email">Email address</label>
<input id="email" name="email" type="email"
aria-invalid="true" aria-describedby="email-error">
<p id="email-error" class="error">Enter an email address in the format name@example.com</p>
aria-invalid="true" tells assistive technology the value is wrong. aria-describedby makes the message part of what's announced when the user lands on the field. Remove both when the value is fixed.
2. Say how to fix it
WCAG 3.3.3 Error Suggestion (Level AA): "If an input error is automatically detected and suggestions for correction are known, then the suggestions are provided to the user."
"Invalid input" is identification without a suggestion. "Enter a date in the future, like 27 March 2027" is both. If your validation code knows what went wrong (too short, wrong format, in the past), put that knowledge in the message. The exception is security: you don't have to say which half of a login was wrong.
3. Make sure people notice
After submit, move focus somewhere useful. For a long form, an error summary at the top with links to each field works well:
<div role="group" aria-labelledby="error-summary-title" tabindex="-1" id="error-summary">
<h2 id="error-summary-title">There are 2 problems with your answers</h2>
<ul>
<li><a href="#email">Enter an email address in the format name@example.com</a></li>
<li><a href="#dob">Enter a date of birth in the past</a></li>
</ul>
</div>
Then call document.getElementById('error-summary').focus() after rendering it. Screen readers read the heading, and every link jumps straight to the field.
For errors that appear without a page change, such as "That username is taken" checked while typing, use a status message. WCAG 4.1.3 Status Messages (AA) asks that such messages "can be programmatically determined through role or properties", which in practice means a live region:
<p id="username-status" role="status"></p>
Put the message into the element with script; don't create the element at the same moment. Save role="alert" for things that really must interrupt, because it cuts off whatever the screen reader was saying.
Common traps
- Toasts that vanish. A message that disappears after a few seconds fails people who read slowly or were busy on another part of the page.
- Placeholder as the only instruction. It disappears as soon as someone types, which is exactly when they need it.
- Errors only on blur. Validating the moment a field loses focus interrupts people who tab through a form to see what it asks for first.
- Clearing the form on error. Re-typing everything is a barrier for everyone and a serious one for some.
How we test forms
AccessiSight clicks the submit button of each form on the page, with every outgoing request blocked so nothing is actually sent, and then checks what happened. Did error text appear, and is it associated with its field? Is aria-invalid set? Was there a status message for script-driven errors? Whether the wording is clear enough is still a human decision, but the mechanics are testable, and they are where most forms fail.
References: Understanding 3.3.1, Understanding 3.3.3 and Understanding 4.1.3.