Skip to content

RESOURCES / BLOG

How to check if an image has loaded in JavaScript?

Front-end apps often need to know exactly when an image is ready so you can swap placeholders, start animations, or measure layout. It sounds simple until you hit race conditions, browser caching, or background images. 

I need to show a loader until my images are fully ready on the page. What is the most reliable way to detect image load success and failure in the browser? Specifically, how to check if an image has loaded in JavaScript? I want something that works both for existing tags and for programmatically created images, plus a safe approach for CSS background images. Any best practices for cached images or lazy loading would be appreciated.

The short version: listen for the load and error events, use the complete and naturalWidth properties for already-cached images, and consider HTMLImageElement.decode() when you care about paint timing.

const img = document.querySelector('img.hero');

function onReady() {
  console.log('Image loaded and has dimensions:', img.naturalWidth, img.naturalHeight);
}

function onFail() {
  console.error('Image failed to load:', img.src);
}

// If the image may already be cached
if (img.complete && img.naturalWidth !== 0) {
  onReady();
} else {
  img.addEventListener('load', onReady, { once: true });
  img.addEventListener('error', onFail, { once: true });
}Code language: JavaScript (javascript)
  • img.complete is true once the fetch has finished.
  • img.naturalWidth lets you differentiate between a cached error and a real image.
function loadImage(url, { timeout = 10000 } = {}) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const t = setTimeout(() => {
      img.src = ''; // Stop downloading
      reject(new Error('Image load timed out'));
    }, timeout);

    img.onload = () => {
      clearTimeout(t);
      resolve(img);
    };
    img.onerror = () => {
      clearTimeout(t);
      reject(new Error('Image failed to load'));
    };
    img.src = url;
  });
}

// Usage
loadImage('/images/hero@2x.jpg')
  .then(img => document.body.appendChild(img))
  .catch(console.error);Code language: JavaScript (javascript)

If an image is already cached, the load event can fire before the browser decodes it for painting. Use img.decode() for an extra guarantee.

async function whenPaintReady(img) {
  if (img.complete && img.naturalWidth !== 0) {
    try { await img.decode(); } catch { /* decode may reject on some errors */ }
    return img;
  }
  await new Promise((res, rej) => {
    img.addEventListener('load', res, { once: true });
    img.addEventListener('error', rej, { once: true });
  });
  try { await img.decode(); } catch {}
  return img;
}Code language: JavaScript (javascript)

Background images do not emit load events. Preload with an Image() object, then apply the background once ready.

function preloadBackground(url) {
  return loadImage(url).then(() => url);
}

preloadBackground('/images/bg.jpg')
  .then(url => {
    document.querySelector('.hero').style.backgroundImage = `url("${url}")`;
  })
  .catch(console.error);Code language: JavaScript (javascript)

Combine IntersectionObserver with the patterns above to defer loading until the image is visible.

const io = new IntersectionObserver(entries => {
  for (const e of entries) {
    if (e.isIntersecting) {
      const img = e.target;
      img.src = img.dataset.src;
      whenPaintReady(img).then(() => {
        img.classList.add('is-ready');
      });
      io.unobserve(img);
    }
  }
});

document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));Code language: JavaScript (javascript)
  • Cached images can make load fire immediately. Check complete first.
  • Prefer robust URLs and CDNs. See a quick explainer on the role of an image URL in delivery pipelines.
  • Good hosting and caching policies improve reliability and time to first paint. For a broader look, read understanding image hosting for websites.

After you have the generic pattern in place, Cloudinary can help you deliver faster and safer. For example, serve optimized formats and quality automatically, which reduces load time variance:

<img
  class="hero"
  alt="Sample"
  src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_1200/sample.jpg"
/>

You can also bootstrap a blurred placeholder, then swap to the full image once your onload logic fires:

<img
  class="hero"
  alt="Sample"
  src="https://res.cloudinary.com/demo/image/upload/w_24,e_blur:2000,q_auto,f_auto/sample.jpg"
  data-src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_1200/sample.jpg"
/>Code language: JavaScript (javascript)

Apply the lazy loading snippet above to replace src with data-src on intersection. This improves real-world performance for your media assets without complicating your code structure.

  • Check img.complete and naturalWidth first, then attach load and error listeners.
  • Use img.decode() when you need paint-ready assurance.
  • For background images, preload with Image() and then set background-image.
  • Combine IntersectionObserver with these patterns for lazy loading and better UX.
  • Optimize delivery with Cloudinary URLs using f_auto and q_auto to speed up perceived load.

Want faster image workflows and simpler delivery at scale? Sign up for Cloudinary and start optimizing today.

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free