Using validateForm() for Form-Level Validationfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Think of your form's buildForm() method as a receptionist collecting a form from a visitor — friendly, unquestioning, just gathering paper. validateForm() is the strict clerk who reviews it before it's accepted: checking nothing's missing, nothing's malformed, nothing's going to cause a problem downstream. Drupal calls this method automatically, on every submission, before submitForm() ever runs — so your submit logic can always trust the data it receives.

What you'll learn in this lesson

  • When and how Drupal calls validateForm() in the form-processing lifecycle
  • How to reject a submission and attach an error message to a specific field
  • Why validation errors stop submitForm() from ever running
  • The difference between form-level validation and the element-specific validation you'll meet in the next lesson

The source file

Path (relative to the Examples module's root): modules/form_api_example/src/Form/SimpleForm.php

public function validateForm(array &$form, FormStateInterface $form_state) {
  $title = $form_state->getValue('title');
  if (strlen($title) < 5) {
    // Set an error for the form element with a key of "title".
    $form_state->setErrorByName('title', $this->t('The title must be at least 5 characters long.'));
  }
}

How it works

Reading a submitted value with $form_state->getValue()

By the time validateForm() runs, every submitted value is already sitting in $form_state, keyed by the element's #name — here, 'title'. You never read raw $_POST data directly in Drupal; getValue() is the one true way in, and it already reflects any AJAX-driven changes made earlier in the request.

setErrorByName() — rejecting the submission

$form_state->setErrorByName('title', $this->t('The title must be at least 5 characters long.'));

This single call does two things at once: it flags the form as invalid (so submitForm() will never be called for this request), and it attaches your message directly under the named element on re-render — the visitor sees exactly which field is wrong, not just a generic "something went wrong" banner. The first argument must match the element's key exactly, or the error message renders at the top of the form instead of next to the field it actually concerns.

Easy to miss: validateForm() is entirely optional. If your form has no validation logic to run, just don't implement the method at all — FormBase's default implementation does nothing, and Drupal proceeds straight from buildForm() to submitForm().

See it for yourself

Visit /examples/form_api_example/simple_form on your own DDEV site, type fewer than 5 characters into the Title field, and submit. You'll land back on the same form with a red error message under that exact field — the submission never reaches submitForm().

Quick check: if validateForm() calls setErrorByName() on two different fields in the same submission, does the visitor see both errors at once, or only the first one? (Answer: both — Drupal collects every error raised during validation and displays all of them together, so visitors can fix everything in one pass instead of being told about problems one at a time.)

Key takeaways

  • validateForm() runs automatically after submission and before submitForm() — you never call it yourself.
  • Read submitted values via $form_state->getValue('element_name'), never raw request data.
  • setErrorByName('element_name', $message) both blocks the submission and attaches the message to that specific field.
  • Multiple validation errors from the same submission all display together, not one at a time.
  • Implementing validateForm() is optional — omit it entirely if a form needs no validation logic.

Coming up next

Form-level validation like this is powerful but centralizes every check in one method. Next, you'll see the alternative: attaching a validation callback directly to a single element with #element_validate, keeping each field's rules right where that field is defined.