Skip to content

How-to guideFor module developers, theme developersDrupal 11

Debug a Drupal site

“Why is my change not showing?” is the most common first-week Drupal question, and the answer is almost always one of two things: a cache served the old output, or an error happened and production-style settings hid it. This page sets up a local site that hides nothing, then gives you the tools for everything the settings don’t catch.

A development configuration where errors display with backtraces, templates and render output are not cached, and you can read logs and attach a debugger. Apply this to local environments only; every switch on this page trades speed and safety for visibility.

Core ships a ready-made local development configuration; you only have to activate it. First, copy the example file into place:

Terminal window
cp web/sites/example.settings.local.php web/sites/default/settings.local.php

Then open web/sites/default/settings.php, scroll to the end, and uncomment the block that loads it. It sits just above the include block that DDEV adds at the bottom of the file:

if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
include $app_root . '/' . $site_path . '/settings.local.php';
}

Rebuild caches (ddev drush cache:rebuild) and your site now runs with the file’s defaults:

  • Verbose errors: error_level is forced to verbose, so exceptions render with a backtrace instead of “The website encountered an unexpected error”.
  • No asset aggregation: CSS and JavaScript are served as individual files, so the file you edit is the file the browser loads.
  • Development services: the file registers web/sites/development.services.yml as an extra container definition. That file enables cacheability debug headers (X-Drupal-Cache-Tags and X-Drupal-Cache-Contexts on responses, see Caching), defines the null cache backend used below, and swaps in a lenient config schema checker that logs schema errors instead of crashing.
  • rebuild.php access: with rebuild_access enabled, visiting /core/rebuild.php rebuilds caches even when the site is too broken to serve pages.

The example file also contains commented-out blocks worth reading: disabling the render cache, page cache, and dynamic page cache by pointing their bins at the null backend. Prefer the form in the next step for that job; it does the same thing without editing PHP.

Visit Administer → Configuration → Development → Development settings/admin/config/development/settings. The form (DevelopmentSettingsForm, provided by the System module) has two switches:

  • Do not cache markup disables the render cache, dynamic page cache, and page cache in one stroke. Your template and render array (the structured arrays Drupal turns into HTML) changes now show on the next request.
  • Twig development mode exposes two checkboxes: Twig debug mode, which enables the dump() function, wraps every rendered template in HTML comments naming the template file and the suggestions Drupal considered, and recompiles templates when they change; and Disable Twig cache, which recompiles every template on every render.

If you do any theming, turn Twig debug mode on now; the template-suggestion comments are the single highest-value debugging feature in Drupal’s front end.

These switches live in the key-value store, not in configuration. That is deliberate: they survive cache rebuilds but never end up in a configuration export, so you cannot accidentally deploy “do not cache markup” to production. The form sets the twig.config parameters for you, so you never edit services.yml by hand for this.

With markup caching off, you still need cache rebuilds, because Drupal also caches discovery: which routes, services, plugins, hooks, and templates exist.

Terminal window
ddev drush cache:rebuild

The rule: editing code inside a function or method needs no rebuild, and neither does editing a template once Twig development mode is on. Anything Drupal discovers needs one: any change to a YAML file (routes, services, libraries, *.info.yml), to a plugin’s attribute, or to the list of hook implementations, and any new file, including a new template. If your new code seems invisible, rebuild before you doubt the code. When the site is too broken for Drush to bootstrap, use vendor/bin/dr cache:rebuild (Drupal 11.4 and later) or /core/rebuild.php.

Anything Drupal catches, it logs. The Standard profile installs the Database Logging module, which records log entries to the database and shows them at Administer → Reports → Recent log messages/admin/reports/dblog. Each entry links to a detail page with the full message and backtrace; filter by type php to see only PHP errors. From the terminal (watchdog is the historical name of Drupal’s database log):

Terminal window
ddev drush watchdog:show --count=20

Two failure classes never reach the database log: fatal errors that kill the request before logging runs, and errors in the logging path itself. For those, read the server logs directly:

Terminal window
ddev logs -s web

Your own code logs through the same pipeline. Inject the logger.factory service (or call $this->getLogger('my_module') in controllers and forms) and the messages show up in the same report; the logging how-to covers patterns and severity levels.

The error display level lives at Administer → Configuration → Development → Logging and errors/admin/config/development/logging: None, Errors and warnings, All messages, or All messages, with backtrace information. It controls display only; logging happens regardless. Note that settings.local.php pins this to verbose with a $config override, which takes precedence over whatever the form saves; the override is what keeps errors visible even after you import production configuration into a local site.

When logs and dump() stop being enough, attach a real debugger. DDEV bundles Xdebug, disabled by default because it slows every request:

Terminal window
ddev xdebug on

Set a breakpoint in your IDE, tell it to listen for PHP debug connections (port 9003), and reload the page. Execution stops at your breakpoint with the full call stack and variables, which is the fastest way to learn how a request actually moves through Drupal: put one breakpoint in your code and walk up the stack. Turn it off when you are done:

Terminal window
ddev xdebug off
  • Trigger an error (for example, add {{ no_such_function() }} to a Twig template you are working on, or visit a page you know throws). You see the exception with a backtrace, not a generic error page.
  • With Twig debug mode on, view any page’s source. You see comments like <!-- THEME DEBUG -->, BEGIN OUTPUT from ..., and a FILE NAME SUGGESTIONS list per template.
  • Run curl -sI https://my-site.ddev.site | grep -i x-drupal. The response includes X-Drupal-Cache-Tags and X-Drupal-Cache-Contexts headers.
  • /admin/reports/dblog shows recent entries, including any error you triggered above.
  • Disable a single cache bin instead of all markup caching: uncomment the relevant block in settings.local.php, for example $settings['cache']['bins']['render'] = 'cache.backend.null';. Useful when you want page caching realistic but render caching out of the way.
  • Test with caches on: before calling work finished, re-enable caching and verify your output carries the right cacheability metadata; the debug headers from development.services.yml show exactly which cache tags and contexts each page has. The caching concept explains how to read them.
  • Caching is the concept behind every “stale output” symptom on this page.
  • Logging covers writing to the log from your own code.
  • Essential tools introduces the Drush and core CLI commands used here.
  • Twig in Drupal builds on Twig debug mode for template work.

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.