Executing Custom Logic in Submit Handlersfor Drupal 8 , 9 , 10 , and 11

Last updated :  

This section has covered the "standard" things a submit handler does — save to config, save an entity, save a file. But a submit handler is really just a PHP method that runs after validated data is available, which means it can trigger absolutely anything: send an email, call a third-party API, queue a background job, invalidate a cache tag. This closing lesson is about that general-purpose flexibility.

What you'll learn in this lesson

  • That a submit handler isn't limited to the "obvious" save patterns
  • How to trigger a background task instead of doing slow work inline
  • Why custom actions still belong in submitForm(), not validateForm()

A submit handler that queues background work

public function submitForm(array &$form, FormStateInterface $form_state) {
  $email = $form_state->getValue('email');

  /** @var \Drupal\Core\Queue\QueueFactory $queue_factory */
  $queue_factory = \Drupal::service('queue');
  $queue_factory->get('my_module_welcome_emails')->createItem(['email' => $email]);

  $this->messenger()->addMessage($this->t('Thanks! A welcome email is on its way.'));
}

Rather than calling a mail-sending service directly inside submitForm() — which would make the visitor's browser wait for that network call to finish before the page responds — this pushes a small item onto a queue (the same Queue API from the Module Development topic) and lets cron process it moments later. The visitor sees an instant response either way.

Why this belongs in submitForm(), not validateForm()

Queueing a welcome email is a genuine side effect — exactly the category of action that has to wait until every validation handler has confirmed the submission is actually going through. Put it in validateForm() instead, and a user could trigger a queued email even on a submission that ultimately gets rejected for a completely unrelated field.

Rule of thumb: if an action can't be cleanly undone (sending an email, calling an external API, charging a payment), it belongs in submitForm() — never in validateForm(), no matter how tempting it is to "get it done early."

Quick check: your form has two validation handlers. The first passes; the second rejects the submission. If your custom action (say, queuing an email) was placed inside the first validation handler instead of submitForm(), what happens? The email gets queued anyway, even though the form submission as a whole was rejected — exactly the bug this lesson's rule of thumb prevents.

Key takeaways

  • A submit handler can trigger literally any PHP logic — it isn't limited to config/entity/file saves.
  • Prefer queueing slow or external work (emails, API calls) rather than running it synchronously inside submitForm().
  • Any action that can't be cleanly undone belongs in submitForm(), never in validateForm().
  • Inject services like queue the same way you would in a controller — \Drupal::service('queue') or proper constructor injection for a form with a create() factory method.

Coming up next

You've now covered the entire submission lifecycle, from a single button to background side effects. Next, the tutorial moves into a more interactive territory: adding AJAX callbacks so a form can respond to the user without a full page reload at all.