Skip to main content

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:

  • load fires when the resource has been successfully downloaded (and, for scripts, executed).
  • error fires when the resource fails to load (network error, 404, CORS block, etc.).

These events work on several types of elements:

ElementLoadsload eventerror event
<script>JavaScript filesYesYes
<img>ImagesYesYes
<link rel="stylesheet">CSS filesYesYes
<iframe>DocumentsYesYes (limited)
<video> / <audio>Media filesYes (via loadeddata)Yes
<object> / <embed>Various resourcesYesYes

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
info

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:

ScenarioTriggers error event?
404 Not FoundYes
Network failure / DNS errorYes
CORS block (no Access-Control-Allow-Origin)Yes
Server returns 500 errorYes
Script loads but contains a syntax errorNo (fires window.onerror)
Script loads but throws a runtime errorNo (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";
warning

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:

  1. The <script> tag must have the crossorigin attribute.
  2. The server hosting the script must respond with the Access-Control-Allow-Origin header.
<!-- 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:

ValueBehavior
"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);
caution

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 attributeServer has CORS headersScript loads?Full error details?
Not setDoes not matterYesNo ("Script error.")
"anonymous"YesYesYes (full details)
"anonymous"NoNo (blocked)N/A
"use-credentials"Yes (with credentials)YesYes
"use-credentials"No / incompleteNo (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 = `
<span>${shortName}</span>
<span class="status">${statusText}</span>
`;

// 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/onerror with a Promise-based wrapper
  • Image onload/onerror with fallback placeholders
  • crossOrigin attribute for cross-origin resources
  • Promise.allSettled to 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:

  • onload fires after the script is downloaded and executed. Globals from the script are available.
  • onerror fires 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 = false on dynamic scripts to maintain execution order.

Image Loading:

  • onload fires when the image is fully downloaded. naturalWidth/naturalHeight are available.
  • onerror fires when the image fails to load.
  • Images start loading when src is set, even before being added to the DOM.
  • Check img.complete for already-cached images.
  • Always set handlers before setting src to 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 send Access-Control-Allow-Origin headers or the script is blocked entirely.
  • For images: crossorigin is 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 load and error to avoid silent failures.
  • Use Promise.allSettled when 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 crossorigin on third-party scripts to get full error details in error monitoring tools.