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.completeis true once the fetch has finished.img.naturalWidthlets 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
loadfire immediately. Checkcompletefirst. - 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.completeandnaturalWidthfirst, then attachloadanderrorlisteners. - Use
img.decode()when you need paint-ready assurance. - For background images, preload with
Image()and then setbackground-image. - Combine IntersectionObserver with these patterns for lazy loading and better UX.
- Optimize delivery with Cloudinary URLs using
f_autoandq_autoto speed up perceived load.
Want faster image workflows and simpler delivery at scale? Sign up for Cloudinary and start optimizing today.