Writing custom validation logic in validateForm()for Drupal 8 , 9 , 10 , and 11

Last updated :  

Some rules simply can't live on a single element. "The end date must be after the start date," "the confirmation password must match the original," "this promo code and this shipping country are incompatible" — none of these can be checked by looking at just one field in isolation. That's exactly what the form-wide validateForm() method is for.

What you'll learn in this lesson

  • How to override validateForm() to write genuinely custom, multi-field validation logic
  • How to read multiple submitted values at once via $form_state->getValue()
  • How to attach an error to a specific field even from form-wide validation
  • What happens if you don't call setErrorByName() at all

Overriding validateForm()

public function validateForm(array &$form, FormStateInterface $form_state) {
  parent::validateForm($form, $form_state);

  $start = $form_state->getValue('start_date');
  $end = $form_state->getValue('end_date');

  if ($start && $end && $end < $start) {
    $form_state->setErrorByName('end_date', $this->t('The end date must be after the start date.'));
  }
}

Unlike an #element_validate callback, validateForm() has the entire form's submitted values available through $form_state at once — you're not limited to one element's own value. This is the right place for any rule that genuinely compares two or more fields against each other.

Always call the parent

parent::validateForm($form, $form_state);

If your form class extends FormBase directly, this line does nothing (the base implementation is empty) — but if you ever extend a more specialized base class like ConfigFormBase or EntityForm, skipping the parent call silently discards validation logic those base classes rely on. Calling it unconditionally, even when it's currently a no-op, protects you against a future refactor that changes what you extend.

setErrorByName() — attaching an error to one named field

$form_state->setErrorByName('end_date', $this->t('The end date must be after the start date.'));

Even though the rule itself needed to look at two fields (start_date and end_date) to evaluate, the error message still gets attached to just one of them — whichever field makes the most sense to the person filling out the form. setErrorByName() takes the field's machine name as a plain string (unlike the element-level setError() from the previous lesson, which takes the render array itself).

No error call means no error, silently. If your validateForm() method runs its checks but never calls setError()/setErrorByName() when a rule fails, Drupal has no way of knowing anything went wrong — the form submits as if everything were valid. The validation logic only matters if it actually reports failures back through the form state.

See it for yourself

Add a validateForm() override comparing a start-date and end-date field, submit with the end date earlier than the start date, and confirm the error appears attached to the end-date field specifically — not as a generic top-of-form message.

Quick check: your rule needs to compare two fields, but you decide to attach the resulting error to just one of them with setErrorByName('end_date', ...). Does this mean the start_date field is somehow exempt from the check? (Answer: no — the comparison itself still considers both fields' values every time; setErrorByName() only controls where the resulting message visually anchors, not which fields the underlying logic actually examined.)

Key takeaways

  • Override validateForm(array &$form, FormStateInterface $form_state) for any rule that needs to compare multiple fields at once.
  • Always call parent::validateForm($form, $form_state) first, even when your base class's implementation is currently a no-op.
  • $form_state->setErrorByName('field_name', $message) attaches an error to one named field from form-wide validation code.
  • A validation rule that never calls setError()/setErrorByName() has no effect — Drupal only blocks submission on an explicit error report.

Coming up next

File uploads bring their own validation concerns — file type, size, and more — that don't fit neatly into either the element-level or form-level patterns you've seen so far. Next, you'll see the dedicated #upload_validators property built specifically for that job.