How to Handle Resource Loading with onload and onerror in JavaScript
When your web page loads external resources like scripts, images, stylesheets, or fonts, you often need to know when they finish loading successfully or if they fail. A dynamically loaded script needs to be fully executed before you can call its functions. An image needs to be fully downloaded before you can draw it on a canvas. And when any resource fails to load, you need to handle the error gracefully instead of leaving the user with a broken page.
The browser fires load and error events on elements that load external resources. These events let you react at exactly the right moment: running code after a script is ready, displaying a fallback when an image fails, or retrying a failed request. Combined with Promises, they form the foundation for robust dynamic resource loading.
This guide covers how load and error events work on scripts and images, how to build reliable loading utilities with proper error handling, and how Cross-Origin Resource Sharing (CORS) affects script loading and error reporting.
load and error Events on External Resources
The browser fires two events on elements that load external content:
loadfires when the resource has been successfully downloaded (and, for scripts, executed).errorfires when the resource fails to load (network error, 404, CORS block, etc.).
These events work on several types of elements:
| Element | Loads | load event | error event |
|---|---|---|---|
<script> | JavaScript files | Yes | Yes |
<img> | Images | Yes | Yes |
<link rel="stylesheet"> | CSS files | Yes | Yes |
<iframe> | Documents | Yes | Yes (limited) |
<video> / <audio> | Media files | Yes (via loadeddata) | Yes |
<object> / <embed> | Various resources | Yes | Yes |
The pattern is the same across all element types: attach a handler for load to run code on success, and a handler for error to handle failure.
let element = document.createElement("script"); // or "img", "link", etc.
element.addEventListener("load", () => {
console.log("Resource loaded successfully!");
});
element.addEventListener("error", () => {
console.log("Resource failed to load!");
});
element.src = "path/to/resource.js"; // Loading begins
document.head.append(element); // For scripts/links: must be in the DOM to load
The load and error events on resource elements track the loading of the external resource itself, not the page. This is different from window.addEventListener("load", ...), which fires when the entire page and all its resources are loaded.
Script Loading: onload and onerror
Dynamically loading scripts is one of the most common uses of resource events. When you create a <script> element and append it to the document, you need to know when the script has finished loading and executing so you can safely use whatever it provides (functions, classes, global variables).
Basic Script Loading
let script = document.createElement("script");
script.onload = function() {
// The script has downloaded AND executed
console.log("Script loaded and executed!");
};
script.onerror = function() {
console.log("Script failed to load!");
};
script.src = "https://cdn.example.com/library.js";
document.head.append(script);
onload Fires After Execution
A critical detail: the load event on a script fires after the script has been both downloaded and executed. This means any variables, functions, or classes defined in the script are available inside the onload handler:
let script = document.createElement("script");
script.onload = function() {
// The script has executed - its exports are available
console.log(typeof jQuery); // "function" (if loading jQuery)
console.log(typeof _); // "function" (if loading Lodash)
console.log(typeof React); // "object" (if loading React)
};
script.src = "https://code.jquery.com/jquery-3.7.1.min.js";
document.head.append(script);
Handling Script Loading Errors
The error event fires when the script cannot be loaded. Common causes include network errors, 404 responses, CORS blocks, and DNS failures. Note that JavaScript errors inside the script do not trigger the error event on the element. They trigger window.onerror instead.
let script = document.createElement("script");
script.onload = function() {
console.log("Loaded successfully");
};
script.onerror = function() {
// This fires for LOADING errors, not JavaScript errors
console.error("Failed to load script: " + script.src);
};
// A URL that does not exist
script.src = "https://cdn.example.com/nonexistent-library.js";
document.head.append(script);
What triggers error on a script element vs. what does not:
| Scenario | Triggers error event? |
|---|---|
| 404 Not Found | Yes |
| Network failure / DNS error | Yes |
CORS block (no Access-Control-Allow-Origin) | Yes |
| Server returns 500 error | Yes |
| Script loads but contains a syntax error | No (fires window.onerror) |
| Script loads but throws a runtime error | No (fires window.onerror) |
Promise-Based Script Loader
Wrapping the load/error pattern in a Promise makes it much easier to work with, especially when loading multiple scripts:
function loadScript(src) {
return new Promise((resolve, reject) => {
let script = document.createElement("script");
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
script.src = src;
document.head.append(script);
});
}
// Usage with async/await
async function initApp() {
try {
await loadScript("https://cdn.example.com/framework.js");
console.log("Framework loaded!");
await loadScript("https://cdn.example.com/plugins.js");
console.log("Plugins loaded!");
// Both scripts have executed - safe to use their exports
startApplication();
} catch (error) {
console.error(error.message);
showErrorMessage("Failed to load required resources. Please refresh the page.");
}
}
initApp();
Loading Multiple Scripts in Parallel
When scripts do not depend on each other, load them in parallel for faster performance:
async function loadAllScripts() {
try {
// All three start downloading simultaneously
let [chart, map, analytics] = await Promise.all([
loadScript("https://cdn.example.com/chart.js"),
loadScript("https://cdn.example.com/map.js"),
loadScript("https://cdn.example.com/analytics.js")
]);
console.log("All scripts loaded!");
initCharts();
initMaps();
initAnalytics();
} catch (error) {
console.error("At least one script failed:", error.message);
}
}
Loading Scripts with Dependencies (Sequential)
When scripts depend on each other (e.g., a plugin that requires its framework), load them sequentially:
async function loadWithDependencies() {
try {
// jQuery must load first
await loadScript("https://code.jquery.com/jquery-3.7.1.min.js");
console.log("jQuery loaded:", typeof jQuery); // "function"
// jQuery UI depends on jQuery
await loadScript("https://code.jquery.com/ui/1.13.2/jquery-ui.min.js");
console.log("jQuery UI loaded:", typeof jQuery.ui); // "object"
// Our app depends on both
await loadScript("/js/app.js");
console.log("App initialized");
} catch (error) {
console.error("Dependency chain broken:", error.message);
}
}
Advanced Script Loader with Caching and Retry
class ScriptLoader {
constructor() {
this.loaded = new Map(); // src → Promise
this.maxRetries = 3;
}
load(src, options = {}) {
// Return cached promise if already loading/loaded
if (this.loaded.has(src)) {
return this.loaded.get(src);
}
let promise = this._loadWithRetry(src, options);
this.loaded.set(src, promise);
return promise;
}
async _loadWithRetry(src, options) {
let retries = options.retries ?? this.maxRetries;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await this._doLoad(src, options);
} catch (error) {
console.warn(`Attempt ${attempt}/${retries} failed for ${src}`);
if (attempt === retries) {
throw new Error(`Failed to load ${src} after ${retries} attempts`);
}
// Wait before retrying (exponential backoff)
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 500)
);
}
}
}
_doLoad(src, options) {
return new Promise((resolve, reject) => {
let script = document.createElement("script");
if (options.crossOrigin) {
script.crossOrigin = options.crossOrigin;
}
if (options.integrity) {
script.integrity = options.integrity;
}
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Network error loading: ${src}`));
script.src = src;
document.head.append(script);
});
}
}
// Usage
let loader = new ScriptLoader();
async function init() {
try {
await loader.load("https://cdn.example.com/chart.js");
// Calling load() again with the same URL returns the cached promise
await loader.load("https://cdn.example.com/chart.js"); // No duplicate request
console.log("Chart library ready");
} catch (error) {
console.error(error.message);
}
}
Checking If a Script Is Already Loaded
Before dynamically loading a script, you might want to check if it is already on the page:
function isScriptLoaded(src) {
// Check if a <script> with this src already exists
return document.querySelector(`script[src="${src}"]`) !== null;
}
function loadScriptOnce(src) {
if (isScriptLoaded(src)) {
return Promise.resolve(); // Already loaded
}
return loadScript(src);
}
// Or check for the global variable the script creates
async function ensureChartJS() {
if (typeof Chart !== "undefined") {
return; // Already available
}
await loadScript("https://cdn.jsdelivr.net/npm/chart.js");
}
Image Loading: onload and onerror
Images fire the same load and error events as scripts. However, images have some unique behaviors you need to account for, particularly around caching and the complete property.
Basic Image Loading
let img = document.createElement("img");
img.onload = function() {
console.log(`Image loaded: ${img.naturalWidth} x ${img.naturalHeight}`);
};
img.onerror = function() {
console.log("Image failed to load");
};
// Setting src starts the download
img.src = "/photos/landscape.jpg";
Loading an Image Before Adding to DOM
Unlike scripts, images start loading as soon as you set their src property, even before they are added to the DOM. This lets you preload images:
let img = new Image(); // Same as document.createElement("img")
img.onload = function() {
// Image is fully loaded - safe to add to page or draw on canvas
document.getElementById("gallery").append(img);
console.log(`Displayed: ${img.naturalWidth}x${img.naturalHeight}`);
};
img.onerror = function() {
console.error("Could not load image");
};
// Download starts immediately (no need to append to DOM first)
img.src = "/photos/landscape.jpg";
Always set event handlers BEFORE setting src. If the image is cached by the browser, onload may fire synchronously (immediately) when src is set. If you set src first and then attach onload, you might miss the event:
// ❌ WRONG: May miss the load event if image is cached
let img = new Image();
img.src = "/photos/cached-image.jpg"; // May fire load synchronously!
img.onload = function() {
console.log("This might never run!");
};
// ✅ CORRECT: Attach handler first
let img = new Image();
img.onload = function() {
console.log("This always runs!");
};
img.src = "/photos/cached-image.jpg";
Handling Cached Images with complete
The img.complete property tells you if the image has already finished loading (e.g., from the browser cache). You should check this after attaching your handlers:
function loadImage(src) {
return new Promise((resolve, reject) => {
let img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
img.src = src;
// Handle already-cached images
if (img.complete) {
// Image was cached - onload may have already fired synchronously,
// or it may fire on the next microtask. Either way, the promise
// pattern handles both cases correctly because resolve() is idempotent
// (calling it twice does nothing the second time).
}
});
}
// Usage
async function displayPhoto() {
try {
let img = await loadImage("/photos/hero.jpg");
document.getElementById("hero-container").append(img);
console.log(`Displayed: ${img.naturalWidth}x${img.naturalHeight}`);
} catch (error) {
console.error(error.message);
// Show fallback
document.getElementById("hero-container").textContent = "Image unavailable";
}
}
Image Preloading
Preload images so they display instantly when needed:
function preloadImages(urls) {
let promises = urls.map(url => loadImage(url));
return Promise.allSettled(promises);
}
// Preload the next page's images while the user reads the current page
async function preloadNextPage() {
let results = await preloadImages([
"/photos/page2-hero.jpg",
"/photos/page2-thumb1.jpg",
"/photos/page2-thumb2.jpg",
"/photos/page2-thumb3.jpg"
]);
let loaded = results.filter(r => r.status === "fulfilled").length;
let failed = results.filter(r => r.status === "rejected").length;
console.log(`Preloaded: ${loaded} succeeded, ${failed} failed`);
}
Displaying Fallback Images on Error
function loadImageWithFallback(primarySrc, fallbackSrc) {
return new Promise((resolve) => {
let img = new Image();
img.onload = () => resolve(img);
img.onerror = () => {
console.warn(`Primary image failed: ${primarySrc}, trying fallback`);
// Try the fallback image
let fallbackImg = new Image();
fallbackImg.onload = () => resolve(fallbackImg);
fallbackImg.onerror = () => {
console.error("Fallback image also failed");
// Create a placeholder
let placeholder = document.createElement("div");
placeholder.textContent = "Image unavailable";
placeholder.style.cssText = `
width: 200px; height: 150px; background: #eee;
display: flex; align-items: center; justify-content: center;
color: #999; border: 1px dashed #ccc; border-radius: 4px;
`;
resolve(placeholder);
};
fallbackImg.src = fallbackSrc;
};
img.src = primarySrc;
});
}
// Usage
async function displayAvatar(userId) {
let element = await loadImageWithFallback(
`/api/users/${userId}/avatar.jpg`,
"/images/default-avatar.png"
);
document.getElementById("avatar-container").append(element);
}
Handling Images Already in the HTML
For images that are already in the HTML (not dynamically created), you need to handle the case where they might have already loaded before your script runs:
function onImageReady(img, callback) {
if (img.complete && img.naturalWidth > 0) {
// Image already loaded (cached or very fast)
callback(img);
} else if (img.complete && img.naturalWidth === 0) {
// Image already attempted to load but failed
callback(null);
} else {
// Image is still loading
img.addEventListener("load", () => callback(img));
img.addEventListener("error", () => callback(null));
}
}
// Usage with existing HTML images
let heroImg = document.getElementById("hero-image");
onImageReady(heroImg, (img) => {
if (img) {
console.log(`Hero image ready: ${img.naturalWidth}x${img.naturalHeight}`);
initParallaxEffect(img);
} else {
console.log("Hero image failed to load");
showPlaceholder();
}
});
Waiting for All Images in a Container
function waitForAllImages(container) {
let images = container.querySelectorAll("img");
let promises = Array.from(images).map(img => {
if (img.complete) {
return img.naturalWidth > 0
? Promise.resolve(img)
: Promise.reject(new Error(`Already failed: ${img.src}`));
}
return new Promise((resolve, reject) => {
img.addEventListener("load", () => resolve(img));
img.addEventListener("error", () => reject(new Error(`Failed: ${img.src}`)));
});
});
return Promise.allSettled(promises);
}
// Usage: Initialize a gallery only after all images are loaded
async function initGallery() {
let gallery = document.getElementById("photo-gallery");
let results = await waitForAllImages(gallery);
let loadedImages = results
.filter(r => r.status === "fulfilled")
.map(r => r.value);
console.log(`${loadedImages.length} of ${results.length} images loaded`);
// Now calculate layout based on actual image dimensions
loadedImages.forEach(img => {
let ratio = img.naturalWidth / img.naturalHeight;
img.style.aspectRatio = String(ratio);
});
}
Loading Images with Progress
While individual image elements do not provide download progress, you can track the overall progress of multiple images:
async function loadImagesWithProgress(urls, onProgress) {
let total = urls.length;
let loaded = 0;
let promises = urls.map(url => {
return loadImage(url).then(img => {
loaded++;
onProgress(loaded, total, url);
return img;
}).catch(error => {
loaded++;
onProgress(loaded, total, url);
throw error;
});
});
return Promise.allSettled(promises);
}
// Usage
loadImagesWithProgress(
["/img/1.jpg", "/img/2.jpg", "/img/3.jpg", "/img/4.jpg", "/img/5.jpg"],
(loaded, total, url) => {
let percent = Math.round((loaded / total) * 100);
console.log(`Loading: ${percent}% (${loaded}/${total})`);
document.getElementById("progress-bar").style.width = `${percent}%`;
}
).then(results => {
console.log("All images processed");
document.getElementById("progress-bar").hidden = true;
});
Cross-Origin Resource Loading (CORS for Scripts)
When you load scripts from a different origin (domain, protocol, or port), the browser applies Cross-Origin Resource Sharing (CORS) rules that affect both loading behavior and error reporting.
The Default: No CORS, Limited Error Info
By default, scripts loaded from another origin work fine (they download and execute normally), but the browser deliberately hides error details for security reasons. If the script contains a JavaScript error, your window.onerror handler gets minimal information:
// Loading a script from another origin WITHOUT crossorigin attribute
let script = document.createElement("script");
script.src = "https://other-domain.com/script-with-bug.js";
document.head.append(script);
// If the script throws an error:
window.addEventListener("error", (event) => {
console.log(event.message); // "Script error." (generic!)
console.log(event.filename); // "" (empty!)
console.log(event.lineno); // 0
console.log(event.colno); // 0
console.log(event.error); // null
});
The browser reports just "Script error." with no details. This is a security measure to prevent websites from extracting sensitive information from scripts on other domains.
Why Error Details Are Hidden
Imagine a bank's website has a script at https://bank.com/account.js that includes user-specific data or error messages. If any website could load that script and read its error details, it could extract private information. The browser prevents this by masking errors from cross-origin scripts.
Enabling Full Error Details with crossorigin
To get full error details from cross-origin scripts, two things must happen:
- The
<script>tag must have thecrossoriginattribute. - The server hosting the script must respond with the
Access-Control-Allow-Originheader.
<!-- Step 1: Add crossorigin attribute to the script tag -->
<script
crossorigin="anonymous"
src="https://cdn.example.com/library.js"
></script>
# Step 2: Server must send this header in the response
Access-Control-Allow-Origin: *
# (or the specific requesting origin)
With both in place, window.onerror receives full error details:
window.addEventListener("error", (event) => {
console.log(event.message); // "ReferenceError: foo is not defined" (full details!)
console.log(event.filename); // "https://cdn.example.com/library.js"
console.log(event.lineno); // 42
console.log(event.colno); // 15
console.log(event.error); // ReferenceError object with stack trace
});
crossorigin Attribute Values
The crossorigin attribute accepts two values:
| Value | Behavior |
|---|---|
"anonymous" | Sends the request without credentials (no cookies, no HTTP auth). The server must respond with Access-Control-Allow-Origin. |
"use-credentials" | Sends the request with credentials (cookies, HTTP auth). The server must respond with Access-Control-Allow-Origin (not *) and Access-Control-Allow-Credentials: true. |
<!-- Most common: anonymous (no cookies sent) -->
<script crossorigin="anonymous" src="https://cdn.example.com/lib.js"></script>
<!-- Shorthand: just "crossorigin" equals "anonymous" -->
<script crossorigin src="https://cdn.example.com/lib.js"></script>
<!-- With credentials (rare, for authenticated CDNs) -->
<script crossorigin="use-credentials" src="https://private-cdn.example.com/lib.js"></script>
Dynamic Script Loading with CORS
When creating scripts dynamically, set the crossOrigin property (note the capital O):
function loadCrossOriginScript(src) {
return new Promise((resolve, reject) => {
let script = document.createElement("script");
// Enable CORS for full error details
script.crossOrigin = "anonymous";
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Failed to load: ${src}`));
script.src = src;
document.head.append(script);
});
}
// Usage
await loadCrossOriginScript("https://cdn.example.com/chart.js");
What Happens When CORS Fails
If you set crossorigin on a script but the server does not send the Access-Control-Allow-Origin header, the script fails to load entirely. This is stricter than the default behavior (no crossorigin), where the script loads fine but with masked error details:
// Without crossorigin: script loads but errors are masked
let script1 = document.createElement("script");
script1.src = "https://no-cors-server.com/script.js";
script1.onload = () => console.log("Loaded (errors will be masked)"); // ✅ Fires
document.head.append(script1);
// With crossorigin: script FAILS if server doesn't support CORS
let script2 = document.createElement("script");
script2.crossOrigin = "anonymous";
script2.src = "https://no-cors-server.com/script.js";
script2.onload = () => console.log("Loaded"); // ❌ Never fires
script2.onerror = () => console.log("CORS error - blocked!"); // ✅ Fires
document.head.append(script2);
Only add crossorigin to scripts hosted on servers that support CORS. Most major CDNs (cdnjs, jsDelivr, unpkg, Google Hosted Libraries) send the correct Access-Control-Allow-Origin: * header. If you are unsure, test without crossorigin first.
Subresource Integrity (SRI)
When loading scripts from a CDN, you can use Subresource Integrity (SRI) to ensure the script has not been tampered with. SRI requires the crossorigin attribute:
<script
src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"
integrity="sha384-OYR8M2VFxWmjCQMfhJkMoMRJCcEKc1v/..."
crossorigin="anonymous"
></script>
If the script's content does not match the hash in the integrity attribute, the browser refuses to execute it and fires the error event:
let script = document.createElement("script");
script.src = "https://cdn.example.com/library.js";
script.integrity = "sha384-expectedHashHere...";
script.crossOrigin = "anonymous";
script.onerror = () => {
console.error("Script integrity check failed! Possible tampering.");
};
document.head.append(script);
CORS for Images
The crossorigin attribute also applies to images. Without it, cross-origin images can be displayed but are tainted, meaning you cannot read their pixel data via <canvas>:
// WITHOUT crossorigin: image displays but canvas becomes tainted
let img = new Image();
img.src = "https://other-domain.com/photo.jpg";
img.onload = () => {
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
ctx.getImageData(0, 0, 1, 1); // ❌ SecurityError: tainted canvas
};
// WITH crossorigin: image can be used freely on canvas
let img2 = new Image();
img2.crossOrigin = "anonymous"; // Must be set BEFORE src
img2.src = "https://other-domain.com/photo.jpg";
img2.onload = () => {
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
ctx.drawImage(img2, 0, 0);
let pixel = ctx.getImageData(0, 0, 1, 1); // ✅ Works
console.log("Pixel data:", pixel.data);
};
Summary of CORS Scenarios for Scripts
crossorigin attribute | Server has CORS headers | Script loads? | Full error details? |
|---|---|---|---|
| Not set | Does not matter | Yes | No ("Script error.") |
"anonymous" | Yes | Yes | Yes (full details) |
"anonymous" | No | No (blocked) | N/A |
"use-credentials" | Yes (with credentials) | Yes | Yes |
"use-credentials" | No / incomplete | No (blocked) | N/A |
Practical Example: Resource Loader with UI Feedback
Here is a complete example that demonstrates loading both scripts and images with proper error handling, progress tracking, and user feedback:
<!DOCTYPE html>
<html>
<head>
<title>Resource Loading Demo</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; }
.resource-list { list-style: none; padding: 0; }
.resource-item {
padding: 12px 16px;
margin: 6px 0;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: monospace;
font-size: 13px;
}
.resource-item.pending { background: #fff3e0; border-left: 4px solid #ff9800; }
.resource-item.success { background: #e8f5e9; border-left: 4px solid #4caf50; }
.resource-item.error { background: #fce4ec; border-left: 4px solid #f44336; }
.status { font-weight: bold; }
.progress-bar-container {
background: #e0e0e0; border-radius: 8px; height: 8px;
margin: 20px 0; overflow: hidden;
}
.progress-bar-fill {
background: #4caf50; height: 100%; width: 0%;
transition: width 0.3s ease;
}
.image-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px; margin-top: 20px;
}
.image-grid img {
width: 100%; height: 120px; object-fit: cover;
border-radius: 8px; border: 2px solid #e0e0e0;
}
.image-placeholder {
width: 100%; height: 120px; border-radius: 8px;
background: #fce4ec; border: 2px dashed #f44336;
display: flex; align-items: center; justify-content: center;
color: #c62828; font-size: 12px;
}
</style>
</head>
<body>
<h1>Resource Loader</h1>
<h2>Scripts</h2>
<button id="load-scripts">Load Scripts</button>
<ul class="resource-list" id="script-list"></ul>
<h2>Images</h2>
<button id="load-images">Load Images</button>
<div class="progress-bar-container" id="progress-container" hidden>
<div class="progress-bar-fill" id="progress-bar"></div>
</div>
<ul class="resource-list" id="image-list"></ul>
<div class="image-grid" id="image-grid"></div>
<script>
// --- Utility Functions ---
function loadScript(src) {
return new Promise((resolve, reject) => {
let script = document.createElement("script");
script.onload = () => resolve({ src, status: "loaded" });
script.onerror = () => reject({ src, status: "error" });
if (new URL(src, location.href).origin !== location.origin) {
script.crossOrigin = "anonymous";
}
script.src = src;
document.head.append(script);
});
}
function loadImage(src) {
return new Promise((resolve, reject) => {
let img = new Image();
img.onload = () => resolve({ src, img, status: "loaded" });
img.onerror = () => reject({ src, status: "error" });
if (src.startsWith("http") && new URL(src).origin !== location.origin) {
img.crossOrigin = "anonymous";
}
img.src = src;
});
}
function addResourceItem(listId, name, status) {
let list = document.getElementById(listId);
let li = document.createElement("li");
li.className = `resource-item ${status}`;
let shortName = name.split("/").pop();
let statusText = status === "pending" ? "⏳ Loading..."
: status === "success" ? "✅ Loaded"
: "❌ Failed";
li.innerHTML = `
`;
// Replace existing item with same name, or add new
let existing = list.querySelector(`[data-src="${name}"]`);
if (existing) {
existing.replaceWith(li);
} else {
li.dataset.src = name;
list.append(li);
}
return li;
}
// --- Script Loading ---
document.getElementById("load-scripts").addEventListener("click", async () => {
let scripts = [
"https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js",
"https://cdn.jsdelivr.net/npm/dayjs@1.11.10/dayjs.min.js",
"https://cdn.example.com/nonexistent.js" // This will fail
];
for (let src of scripts) {
addResourceItem("script-list", src, "pending");
}
let results = await Promise.allSettled(
scripts.map(src =>
loadScript(src).then(result => {
addResourceItem("script-list", src, "success");
return result;
}).catch(error => {
addResourceItem("script-list", src, "error");
throw error;
})
)
);
let loaded = results.filter(r => r.status === "fulfilled").length;
console.log(`Scripts: ${loaded}/${scripts.length} loaded`);
// Verify loaded libraries
if (typeof _ !== "undefined") console.log("Lodash version:", _.VERSION);
if (typeof dayjs !== "undefined") console.log("Day.js loaded:", dayjs().format());
});
// --- Image Loading ---
document.getElementById("load-images").addEventListener("click", async () => {
let images = [
"https://picsum.photos/300/200?random=1",
"https://picsum.photos/300/200?random=2",
"https://picsum.photos/300/200?random=3",
"https://invalid-domain-12345.com/nope.jpg", // This will fail
"https://picsum.photos/300/200?random=4"
];
let progressContainer = document.getElementById("progress-container");
let progressBar = document.getElementById("progress-bar");
let imageGrid = document.getElementById("image-grid");
progressContainer.hidden = false;
progressBar.style.width = "0%";
imageGrid.innerHTML = "";
for (let src of images) {
addResourceItem("image-list", src, "pending");
}
let completed = 0;
let results = await Promise.allSettled(
images.map(src =>
loadImage(src).then(result => {
completed++;
progressBar.style.width = `${(completed / images.length) * 100}%`;
addResourceItem("image-list", src, "success");
// Add image to grid
imageGrid.append(result.img);
result.img.style.width = "100%";
result.img.style.height = "120px";
result.img.style.objectFit = "cover";
result.img.style.borderRadius = "8px";
return result;
}).catch(error => {
completed++;
progressBar.style.width = `${(completed / images.length) * 100}%`;
addResourceItem("image-list", src, "error");
// Add placeholder to grid
let placeholder = document.createElement("div");
placeholder.className = "image-placeholder";
placeholder.textContent = "Failed";
imageGrid.append(placeholder);
throw error;
})
)
);
let loaded = results.filter(r => r.status === "fulfilled").length;
console.log(`Images: ${loaded}/${images.length} loaded`);
});
</script>
</body>
</html>
This example demonstrates:
- Script
onload/onerrorwith a Promise-based wrapper - Image
onload/onerrorwith fallback placeholders crossOriginattribute for cross-origin resourcesPromise.allSettledto handle mixed success/failure results- Progress tracking across multiple resource loads
- Real-time UI updates as each resource loads or fails
Summary
The load and error events on resource elements let you respond to successful and failed resource loading.
Script Loading:
onloadfires after the script is downloaded and executed. Globals from the script are available.onerrorfires when the script fails to download (not for JavaScript errors inside the script).- Always set handlers before setting
src. - Wrap in Promises for clean async workflows.
- Use
script.async = falseon dynamic scripts to maintain execution order.
Image Loading:
onloadfires when the image is fully downloaded.naturalWidth/naturalHeightare available.onerrorfires when the image fails to load.- Images start loading when
srcis set, even before being added to the DOM. - Check
img.completefor already-cached images. - Always set handlers before setting
srcto avoid missing cached image events.
Cross-Origin (CORS):
- Without
crossorigin: scripts load normally but error details are masked ("Script error."). - With
crossorigin="anonymous": full error details are available, but the server must sendAccess-Control-Allow-Originheaders or the script is blocked entirely. - For images:
crossoriginis needed to use cross-origin images on<canvas>without tainting. - Most CDNs support CORS. Set
crossorigin="anonymous"on CDN-hosted scripts for better error monitoring.
Best Practices:
- Use Promise-based wrappers for clean, composable resource loading.
- Always handle both
loadanderrorto avoid silent failures. - Use
Promise.allSettledwhen loading multiple resources that should not fail as a group. - Add retry logic for unreliable resources.
- Cache load promises to prevent duplicate requests for the same resource.
- Set
crossoriginon third-party scripts to get full error details in error monitoring tools.