Hero image for Error Messages as a Computable Contract: Text, Bound, Flagged, Corrective

Error Messages as a Computable Contract: Text, Bound, Flagged, Corrective

This post is part of the Design Agent series on one question: what criteria can an agent use to judge design? Error messages make a useful test case. They look like copywriting, so people usually judge them by taste. This post turns that taste test into four clauses that can be checked against the DOM.

The question behind the question

A useful error message says what went wrong and how to fix it. WCAG 3.3.1 requires the first part: an automatically detected input error must be identified and described to the user in text. WCAG 3.3.3 requires the second, where correction suggestions are known.

WCAG gives a pair of examples. “Email is not valid” passes 3.3.1. “Please provide a valid email address in the format [email protected]” also conveys how to fix the problem and passes both criteria, according to the 3.3.1 Understanding page.

In this post, a computable contract means a standard whose clauses can be checked mechanically. That is the move from taste to criteria: each clause must be something a checker can assert or something it must flag for judgment.

The contract: four checks, one DOM snapshot

The error-message contract has four clauses. Each error must be:

  • (a) Text. The error is identified and described in text.
  • (b) Bound. The error is programmatically connected to its field.
  • (c) Flagged. The field is marked invalid in the accessibility tree.
  • (d) Corrective. The error tells the user how to fix the problem.

Clauses (a) and (d) live in the string. Clauses (b) and (c) live in markup and runtime wiring. A DOM check can assert clauses (a) through (c) structurally. Clause (d) is a judgment about wording. The clauses most likely to be missing from generated markup are (c) and (d). That split is this post’s main point, and it is reasoning from how the checks are structured, not a measured result.

Check 1: The error is text

WCAG 3.3.1 requires that the item in error be identified and the error described in text. The intent section says an unsuccessful submission must not simply re-display the form without a hint that it failed. Color and icons may be added, but the text is the required part.

For the programmatic side, MDN’s aria-describedby reference says the attribute is appropriate when the associated content is plain text. Its note says the content “should only be a text string.” When an error has structure or navigable semantics, MDN recommends aria-details instead.

An agent can check this clause by confirming that each error node contains non-empty text. It cannot confirm from the DOM alone that the text is accurate.

Check 2: The error is bound to its field

The error should be connected to its input. The W3C tutorial uses aria-describedby for this. MDN describes its value as an ID or a space-separated list of IDs. It must reference elements in the same DOM document, because it is not designed to reference external resources. The referenced elements do not need to be visible.

The W3C WAI form notifications tutorial says form fields can be associated with their error messages using aria-describedby. The same tutorial says each entry in an error list should link in-page to its control.

MDN’s aria-invalid reference pairs aria-errormessage with invalid state for custom validation. An audit can accept either as a binding signal. The binding is what makes the text attributable to the field.

Check 3: The field is marked invalid

The field should carry aria-invalid="true" when validation fails. MDN says the attribute should be set with JavaScript as a result of a validation process. It also says that if a value is invalid, the author should set aria-invalid="true" and tell the user there is an error.

The token rules matter for any checker:

  • The defined values are true, false, grammar, and spelling.
  • The default is false.
  • Any value not in the list is treated as true.

That last rule has an audit consequence. A typo such as "flase" is treated as invalid, so an audit must match exact tokens.

Timing matters too. MDN says not to set aria-invalid="true" on empty required elements until the user attempts to submit, because they may still be filling the form in.

For the programmatic side, MDN’s Constraint Validation API page says setCustomValidity(message) adds a custom error message, and the element is then considered invalid. That is constraint-validation state, and it does not by itself set aria-invalid. Calling setCustomValidity("") renders the input valid again. The page also says validationMessage returns a localized message describing unmet constraints, or an empty string if the control is valid.

Native validation contributes to this clause, with limits. MDN’s aria-invalid page says that when a user submits a form with errors, the first invalid control displays an error message natively. It also says native messaging varies by browser and cannot be styled.

Check 4: The error gives a correction

WCAG 3.3.3 requires that when an input error is detected and suggestions for correction are known, the suggestions be provided, unless that would jeopardize security or purpose. The Understanding page explains that users with cognitive limitations may find it hard to work out how to fix an error, even when they know one occurred.

Here is the clause boundary in WCAG’s own strings. “Email is not valid” passes 3.3.1 only. “Please provide a valid email address in the format [email protected]” passes both 3.3.1 and 3.3.3. The difference is a correction hint, and it sits in the text.

No DOM attribute records whether a message is helpful. A checker can confirm that a correction string exists in the expected place. It cannot confirm that the string actually tells the user what to do. The W3C tutorial says error messages should “provide guidance on how to correct mistakes,” which is a content judgment.

Two messages, one audit

The table compares WCAG’s two example messages against the four clauses and both success criteria. Clauses (b) and (c) are marked as wiring-dependent. The message text alone does not settle them. WCAG 3.3.3 applies where a correction is known, so its row reads “not met” for a message that offers none.

Contract clause “Email is not valid” “Please provide a valid email address in the format [email protected]”
(a) Text Pass: it is text (WCAG 3.3.1) Pass: it is text (WCAG 3.3.1)
(b) Bound to its field Not set by the string. Needs aria-describedby or aria-errormessage wiring (MDN aria-describedby) Same. Needs the same wiring
(c) Marked invalid Not set by the string. Needs aria-invalid (MDN aria-invalid) Same. Needs the same wiring
(d) Gives a correction Not met: names the error, offers no fix (WCAG 3.3.3) Pass: conveys how to fix it (WCAG 3.3.1)
WCAG 3.3.1 Pass Pass
WCAG 3.3.3 Not met (no correction offered) Pass

Two clauses belong to the markup, not the message. That is why an audit has to run on the DOM and not only on the string.

A pattern that satisfies all four clauses

The W3C WAI tutorial combines several techniques that map onto the contract. Its guidance, summarized:

  • List errors before the form, under a distinctive heading.
  • Put the error list in a container with role="alert" when the page updates dynamically, so assistive technology users notice the change.
  • For each entry, reference the control’s label, give a concise description, explain how to correct the mistake, and include an in-page link to the control.
  • Associate each field with its message using aria-describedby.
  • When submission fails, move focus to the first input that contains an error. The tutorial calls this “convenient.”
  • For format-sensitive inputs such as dates, check on focus change rather than while typing, since typing-time checks would show errors most of the time.

The snippet below shows the markup an audit would inspect. It is illustrative, not a complete form.

<label for="email">Email</label>
<input id="email" type="email" aria-describedby="email-error" aria-invalid="true">
<p id="email-error">Please provide a valid email address in the format [email protected]</p>

What the audit sees: a described field, a resolvable ID, an explicit invalid token, and a corrective text string.

What a rule can assert, and what it cannot

A DOM-based rule can check the attribute-level clauses. The W3C ACT rules include a proposed test for the description part of 3.3.1 (Error message describes invalid form field value). That test is narrower than message quality. A DOM-based rule can assert the following:

  • Every field that can be invalidated has aria-describedby, and each ID in the list resolves to an element in the same document (MDN).
  • Each associated error node has non-empty text (WCAG 3.3.1).
  • After a submit attempt, aria-invalid holds one of the exact tokens true, false, grammar, or spelling.
  • The initial markup does not hardcode aria-invalid="true" on empty required fields.
  • The error summary has a heading, and its entries link to controls. When errors are inserted dynamically, the summary carries role="alert" (W3C WAI).

A DOM-based rule cannot assert the following:

  • Whether a correction is helpful. The DOM proves that a string exists, not that it tells the user how to fix the problem. Heuristics such as message length are proxies, and proxies fail in both directions.
  • Whether the error text matches the real error. A message can say “invalid email” on a date field.
  • What a screen reader announces. The DOM shows that state was set, not the rendered experience.
  • Whether the validation flow ever fires. A static check sees markup, not behavior. The Constraint Validation API calls happen at runtime (MDN).

For agents, three of the four clauses are computable. The fourth needs a proxy or a human judgment. Knowing which clause is which is itself a design criterion.

Why generated forms may miss clauses (c) and (d) quietly

This section is a reasoned expectation from the structure of the checks. It is not a measured finding. No AI-generated forms were audited for this post.

Clause (a) is easy for a generator, since it writes text. Clause (b) is a static idiom it can emit. Clause (c) requires runtime wiring, because aria-invalid is set as a result of validation (MDN). Static generation has little occasion to perform that step. Clause (d) is our reading of the criterion: it depends on wording that no attribute in the markup records.

The failure would be quiet. The form would still render, validate, and show text. Nothing in the markup would raise an error, so a missing clause would produce no visible warning. A contract makes that silent gap checkable.

Limitations

  • This review is documentation-based. It does not include live-site checks or audits of AI-generated forms, so the clause (c) and (d) failure point is an expectation.
  • The four checks are necessary, not sufficient. A form can pass all four with a correction that is present but weak. The contract guarantees wiring, not quality.
  • Clause (c) is runtime state. A DOM snapshot proves state only at one moment, and static analysis cannot confirm that the validation flow sets it.
  • The aria-invalid token rule has an audit consequence. Non-token values are treated as true, so checkers must match exact tokens. A passing token does not prove the intended semantics.
  • Scope covers WCAG 3.3.1, WCAG 3.3.3, and the W3C WAI tutorial pattern. Other form-accessibility requirements are out of scope.
  • Native validation supports clause (c) only in part. MDN notes that messages vary by browser and cannot be styled, and WCAG 3.3.1 notes that only the first error is exposed per submit.

FAQ

The five questions below come up when applying the four checks. Each answer is drawn from the sources cited.

Is native HTML validation accessible?

WCAG 3.3.1 says that in most common user agent and screen reader combinations, the screen reader announces the error message and the field name, which meets the criterion. It lists disadvantages: messages can be temporary, generic, and not magnified with zoom, and only the first error is exposed per submit. It asks developers to consider whether native validation is accessibility supported.

Should I set aria-invalid=“true” on required fields when the page loads?

No. MDN says to set aria-invalid with JavaScript as a result of validation. It also says not to set aria-invalid="true" on empty required elements until the user attempts to submit. Use only the exact tokens, because any other value is treated as true.

Should I use aria-describedby or aria-errormessage?

The W3C WAI tutorial uses aria-describedby to associate fields with errors. MDN pairs aria-errormessage with aria-invalid. An audit can accept either. Any aria-describedby target must be plain text in the same document (MDN).

What should happen when a submission fails?

The W3C WAI tutorial recommends listing errors before the form under a distinctive heading. Each entry should reference the control’s label, describe the error concisely, explain the correction, and link in-page to the control. The list should use role="alert" when the page updates dynamically. Focus can move to the first invalid input.

Can a linter tell me whether my error message is helpful?

No. A linter can check that error text exists, that IDs resolve, and that invalid state is set. Whether a correction helps is a content judgment. WCAG 3.3.3 asks for suggestions, but a DOM check does not record whether a suggestion is useful. That conclusion is reasoning from the criterion’s wording.

The Bottom Line

Every error should be text, attached to its field, flagged invalid, and corrective. Three of those clauses can be checked in the DOM, and the fourth needs judgment. The two WCAG examples differ by one clause: the correction. “Email is not valid” passes 3.3.1 only, and the longer message passes both criteria. That single clause is the one no DOM check can grade.

How This Guide Was Built

This guide is based on six public W3C and MDN documents. This review is based on official documentation — we did not run these checks against live sites. The sources were the WCAG 3.3.1 Understanding page, the WCAG 3.3.3 Understanding page, the W3C WAI form notifications tutorial, and MDN’s reference pages for aria-invalid, aria-describedby, and the Constraint Validation API.

Every “what an audit would find” statement is a reading of documented behavior, not a measured result.