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.
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()callssetErrorByName()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 beforesubmitForm()— 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.