Skip to content

Anti-Patterns & Common Mistakes

When to Use

Reference when reviewing block code or debugging unexpected behavior. Every pattern here has been shipped in production and caused real problems.

Decision

Anti-Pattern Why It's Wrong Correct Approach
Using \Drupal::service() in blocks Not testable, tight coupling, bypasses DI Inject services via ContainerFactoryPluginInterface
max-age = 0 for varying content Kills performance, database hit every request Use cache contexts to vary cache properly
Business logic in templates Not testable, violates MVC, hard to maintain Put logic in build() or preprocess
Returning HTML strings from build() Not themeable, security risk (XSS), breaks render pipeline Return render arrays with #theme or #markup (sanitized)
Using echo/print in block plugin Output appears before page render, breaks layout Return render arrays
Not checking access in custom rendering Security hole, bypasses access control Call blockAccess() or use entity access checks
Hardcoding entity IDs Breaks across environments, fragile Use UUIDs or entity queries
Creating content blocks for dynamic data Database overhead, editor confusion Use block plugins for logic-driven content
Creating block plugins for static content Code changes required for edits, inflexible Use content blocks for editor-managed content
Placing blocks in code when config works Not site-builder friendly, requires deploys Use Block config entities
Over-using AccessResult::forbidden() Uncacheable, performance hit Use neutral() or allowedIf() with contexts
Not merging cache metadata Loses parent cache tags/contexts, incorrect caching Use Cache::mergeTags(), Cache::mergeContexts()
Ignoring empty results Renders empty wrapper with class/ID Return [] when no content
Complex queries in build() without lazy builder Slow page loads, defeats BigPipe Use #lazy_builder for expensive operations

Pattern

Anti-pattern: Static service calls

// WRONG
public function build() {
  $nodes = \Drupal::entityTypeManager()
    ->getStorage('node')
    ->loadByProperties(['type' => 'article']);
}

// RIGHT
public function __construct(..., EntityTypeManagerInterface $etm) {
  $this->entityTypeManager = $etm;
}
public function build() {
  $nodes = $this->entityTypeManager
    ->getStorage('node')
    ->loadByProperties(['type' => 'article']);
}

Anti-pattern: max-age = 0

// WRONG - hits database every request
public function build() {
  $user = User::load(\Drupal::currentUser()->id());
  return ['#markup' => $user->getDisplayName(), '#cache' => ['max-age' => 0]];
}

// RIGHT - cache per user
public function build() {
  $user = User::load($this->currentUser->id());
  return ['#markup' => $user->getDisplayName(), '#cache' => ['contexts' => ['user']]];
}

Anti-pattern: HTML in build()

// WRONG - not themeable, XSS risk
return ['#markup' => '<div class="my-block"><h2>' . $title . '</h2></div>'];

// RIGHT - themeable, safe
return ['#theme' => 'my_block', '#title' => $title]; // Auto-escaped in template

Anti-pattern: Not handling empty

// WRONG - renders empty wrapper
public function build() {
  $items = $this->getItems();
  return ['#theme' => 'item_list', '#items' => $items];
}

// RIGHT - no wrapper when empty
public function build() {
  $items = $this->getItems();
  if (empty($items)) {
    return [];
  }
  return ['#theme' => 'item_list', '#items' => $items];
}

Anti-pattern: Forgetting cache merge

// WRONG - loses parent cache metadata
public function getCacheTags() { return ['node:1']; }

// RIGHT - preserves parent tags
public function getCacheTags() {
  return Cache::mergeTags(parent::getCacheTags(), ['node:1']);
}

Common Mistakes

  • Wrong: "It works on my machine" without testing caching → Right: Works uncached, fails in production
  • Wrong: Not profiling block performance → Right: 500ms query in block destroys page load time
  • Wrong: Copying patterns from Drupal 7 → Right: D7 patterns don't apply; use D10+ APIs
  • Wrong: Not reading core examples → Right: Core blocks demonstrate best practices
  • Wrong: Skipping code review → Right: Blocks affect every page; errors expensive

See Also