Blog Implementation Guides 9 min read

Angular Image Fallback: onerror, Placeholders, and Error States

Implement an angular image placeholder directive that catches src load errors, swaps in fallback URLs, and prevents infinite onerror cycles across Angular templates.

Angular image placeholderAngular directiveonerror fallbackImage error handlingAngular templates
Angular Image Fallback: onerror, Placeholders, and Error States

Angular does not handle image load errors by default. A reusable attribute directive is the cleanest way to add angular image placeholder behavior across your templates without duplicating event binding logic in every component.

This guide walks through the directive implementation, the infinite-loop guard, TypeScript decorators, integration with NgOptimizedImage, and how to choose fallback.pics URLs that match your slot dimensions.

Problem

How Angular handles image load failures by default

Angular template bindings are reactive but they do not catch network-level failures like a 404 response for an image src. If you bind [src] to a string that points to a missing resource, the browser fires an error event on the img element and renders the broken-image icon. Angular has no built-in mechanism to intercept this.

The common workaround of adding (error) bindings directly in each template works, but it scatters the logic everywhere and makes it hard to change the fallback strategy globally. Every team eventually ends up with slightly different implementations across product components, blog cards, and user profile views.

An attribute directive centralizes the pattern. You add fallbackSrc as an attribute once and the directive handles the error event, the loop guard, and the src swap. You change the default behavior in one place, not across dozens of components.

Directive

The FallbackSrc directive for angular image placeholder

The directive uses @HostListener to bind to the error event on the host element. When the image fails, it checks the errored flag, sets it to true, and updates the src property directly on the element reference. Direct DOM mutation is fine here because this is an intentional error-recovery pattern.

The @Input fallbackSrc property receives the replacement URL. Mark the errored flag as a private instance property so each directive instance tracks its own error state independently across multiple images on the same page.

Implementation tsx
// fallback-src.directive.ts
import {
  Directive, Input, HostListener, ElementRef
} from '@angular/core';

@Directive({ selector: 'img[fallbackSrc]', standalone: true })
export class FallbackSrcDirective {
  @Input() fallbackSrc = '';
  private errored = false;

  constructor(private el: ElementRef<HTMLImageElement>) {}

  @HostListener('error')
  onError() {
    if (!this.errored && this.fallbackSrc) {
      this.errored = true;
      this.el.nativeElement.src = this.fallbackSrc;
    }
  }
}

Template usage

Using the directive in Angular component templates

Add the directive to the imports array of your standalone component or to the declarations array of your NgModule. Then apply the fallbackSrc attribute to any img element. The attribute name doubles as the CSS selector, so no extra configuration is needed.

Match the fallback URL dimensions to the img element's width and height attributes. This prevents a second layout shift when the fallback renders at a different intrinsic size than the intended image.

Implementation text
<!-- product-card.component.html -->
<img
  [src]="product.imageUrl"
  [alt]="product.name"
  fallbackSrc="https://fallback.pics/api/v1/400x300/E5E7EB/6B7280?text=No+Image"
  width="400"
  height="300"
/>

<!-- user avatar -->
<img
  [src]="user.avatarUrl"
  [alt]="user.name"
  fallbackSrc="https://fallback.pics/api/v1/avatar/48?text={{ user.initials }}"
  width="48"
  height="48"
  class="rounded-full"
/>

Loop guard

Preventing infinite error loops in Angular directives

The errored flag is the same technique used in React and Vue fallback implementations. Without it, setting el.nativeElement.src to a fallback URL triggers another load attempt. If that URL also fails—due to offline state, CSP rules, or an expired domain—the error event fires again and you are in an infinite loop.

The guard is a simple private boolean. After the first error, set it to true and never overwrite src again for this element. If you want to retry when the input URL changes (e.g., due to Angular change detection from a new data binding), reset errored in ngOnChanges when the relevant input changes.

Implementation tsx
// Extended version with reset on input change
import { Directive, Input, HostListener,
         ElementRef, OnChanges, SimpleChanges } from '@angular/core';

@Directive({ selector: 'img[fallbackSrc]', standalone: true })
export class FallbackSrcDirective implements OnChanges {
  @Input() src = '';
  @Input() fallbackSrc = '';
  private errored = false;

  constructor(private el: ElementRef<HTMLImageElement>) {}

  ngOnChanges(changes: SimpleChanges) {
    if (changes['src']) {
      this.errored = false;
    }
  }

  @HostListener('error')
  onError() {
    if (!this.errored && this.fallbackSrc) {
      this.errored = true;
      this.el.nativeElement.src = this.fallbackSrc;
    }
  }
}

NgOptimizedImage

Fallbacks alongside Angular NgOptimizedImage

Angular 15+ includes NgOptimizedImage which automates lazy loading, intrinsic size hints, and LCP preload hints. It does not provide an error fallback mechanism out of the box. You can still use the FallbackSrcDirective on the same element, but NgOptimizedImage uses ngSrc rather than src, so the directive needs to listen to the error event via @HostListener and then set nativeElement.src directly.

An alternative is to wrap NgOptimizedImage in a parent component that uses a template reference variable and an (error) output binding if and when Angular exposes one. Until then, the native DOM event approach on the element itself is the most reliable path.

Testing

Testing the FallbackSrc directive in Karma or Jest

Create a test component that uses the directive, set the src to a URL that will fail (a data: URL that is syntactically invalid works reliably), and dispatch an error event programmatically. Assert that nativeElement.src equals your expected fallback URL.

Test the loop guard by dispatching a second error event after the first. The src should still equal the fallback URL and not change again. This protects against future edits that accidentally remove the guard.

Implementation text
it('swaps to fallback on error', () => {
  const fixture = TestBed.createComponent(TestHostComponent);
  fixture.detectChanges();
  const img = fixture.nativeElement.querySelector('img');

  img.dispatchEvent(new Event('error'));
  fixture.detectChanges();

  expect(img.src).toContain('fallback.pics');
});

it('does not loop on second error', () => {
  const img = fixture.nativeElement.querySelector('img');
  img.dispatchEvent(new Event('error'));
  img.dispatchEvent(new Event('error'));
  fixture.detectChanges();

  // src should equal the fallback, not change again
  expect(img.src).toContain('fallback.pics');
});

Key takeaways

What to standardize before shipping

  • A standalone directive with @HostListener('error') centralizes the fallback pattern without scattering it across templates.
  • Set a private errored boolean flag to prevent the infinite loop when the fallback URL also fails.
  • Reset the errored flag in ngOnChanges when the src input changes to allow fresh load attempts.
  • Match fallback URL dimensions to the img width/height attributes to avoid a second layout shift.
  • Test the directive by dispatching error events programmatically and asserting the final src value.

Production fallback layer

Use fallback.pics anywhere an image URL is accepted.

Start with one deterministic URL and standardize fallback behavior across your design system.

Read the docs