Fallback image if image is not found in picture source tag

How to fall back to another image when an avif or webp source in a picture tag returns a 404

Velu S Gautam3 min readLearnings

When you render a big list of image paths coming from a database, some of them will 404. Offering avif and webp through <picture> lets the browser pick the best format it supports, but there is a catch.

The browser picks a <source> based on its type and sticks with it. It never checks if the file actually exists. So if photo.avif is supported but returns a 404, the browser will not try the next <source> and will not use the <img src> either. You end up with a broken image, even though a perfectly good JPEG is sitting right there in the <img> tag.

There is no pure HTML or CSS way to handle this, so we need a small error handler.

Remove the failed source on error

The error event fires on the <img>, not on the <source>. When it fires, remove the <source> that failed. Removing a source makes the browser run image selection again, so it tries the next source (webp after a broken avif, for example) and finally the <img src>.

JAVASCRIPT
<script>
  function fallbackToImg(img) {
    const sources = [...img.parentNode.querySelectorAll('source')];
    if (!sources.length) {
      img.onerror = null; // the <img src> itself failed, nothing left to try
      return;
    }
    // find the source the browser picked, or just take the first one
    const failed =
      sources.find((s) =>
        s.srcset
          .split(',')
          .some((c) => new URL(c.trim().split(/\s+/)[0], document.baseURI).href === img.currentSrc)
      ) || sources[0];
    failed.remove();
  }
</script>

<picture>
  <source srcset="photo.avif" type="image/avif" />
  <source srcset="photo.webp" type="image/webp" />
  <img
    src="fallback.jpeg"
    alt="A description of the image"
    width="640"
    height="480"
    loading="lazy"
    decoding="async"
    onerror="fallbackToImg(this)"
  />
</picture>

Every failure removes one source and fires another error if the next one is broken too. So it walks down the list: avif, then webp, then the plain <img src>. It works with any number of sources in any order.

If the fallback image also 404s, there are no sources left, so onerror is set to null and it stops. No infinite loop. You can show a placeholder at that point if you want.

What you get in each situation

Take the markup above, with photo.avif and photo.webp as sources and fallback.jpeg in the <img>. This is what ends up on screen depending on which files actually exist on your server:

photo.avifphoto.webpfallback.jpegImage shown
existsexistsexistsavif
missingexistsexistswebp
existsmissingexistsavif (the broken webp is never requested)
missingmissingexistsjpeg
missingmissingmissingbroken image, and it stops there

A few things worth knowing:

  • The order of the <source> tags does not matter for the fallback. If webp is listed first and is missing, avif is used, the same way.
  • Adding more sources (a third format, for example) works too. Each broken one is removed and the next one is tried.
  • In the last row the handler runs out of sources, sets onerror to null and stops, so a missing fallback never causes a loop.

If inline handlers are blocked (strict CSP)

If your Content Security Policy blocks inline scripts, the onerror="..." attribute will not run. Attach the listener from a script instead and reuse fallbackToImg from above. The error event does not bubble, so it has to be added on each <img>.

JS
document.querySelectorAll('picture img').forEach((img) => {
  img.addEventListener('error', () => fallbackToImg(img));

  // the image may have already failed before this script ran
  if (img.complete && img.naturalWidth === 0) fallbackToImg(img);
});

The img.complete && img.naturalWidth === 0 check is important. With server rendering, hydration or deferred scripts, the 404 can happen before your listener exists, and then the error event is gone. Checking afterwards catches those images.

A few small things

  • Always set width and height so the page does not jump when the image swaps.
  • Write a real alt for content images, and use alt="" only for decorative ones.
  • avif works in all current browsers now (Safari since 16.4), so the JPEG fallback is mostly there for missing files rather than old browsers.

I tested all of the cases above in Chrome with real avif and webp files, plus the version without inline handlers. I did not test other browsers. It relies on standard image selection, which runs again when a <source> is removed, so it should behave the same in Safari and Firefox, but I have not checked.


Thanks for reading.


Cheers ✌🏻

Comments