Resource loading: onload and onerror
A web page rarely arrives in one piece. Scripts, images, iframes, stylesheets — each is a separate download that finishes on its own schedule, and often after your code has already started running. The browser gives you two events to find out how each of those downloads went:
load— the resource downloaded fine (and, for a script, ran to completion).error— the download failed.
Get a handle on these two and you can build reliable loading logic: run code the moment a library is ready, swap in a fallback image when one 404s, or report the failure to a monitoring service.
script / img / iframe
success
failed
Loading a script
Say you want to pull in a third-party library at runtime and then call a function it defines. You can create the <script> element yourself and drop it into the document:
let script = document.createElement('script');
script.src = "my.js";
document.head.append(script);
The moment that element lands in the document, the browser starts fetching my.js. But the fetch is asynchronous. The next line of your code runs immediately, long before the download is done — so any function declared inside my.js does not exist yet. Call it too early and you get a ReferenceError.
You need a signal that says “the script is ready now.” That signal is the load event.
script.onload
The load event fires after the script has been downloaded and executed. By the time your handler runs, everything the script declared — variables, functions, globals — is available.
let script = document.createElement('script');
// can load any script, from any domain
script.src = "https://cdnjs.cloudflare.com/ajax/libs/mathjs/11.11.0/math.js"
document.head.append(script);
script.onload = function() {
// the script has run, so its global "math" now exists
alert( math.version ); // shows the library version
};
Inside onload you are on safe ground: reach for the library’s variables, call its functions, wire up the rest of your app that depended on it.
Watch the async gap for yourself. The demo below adds a <script> that defines a global confetti function. (A real page would point src at a URL; here the code is inlined as a data: source so nothing hits the network — the loading behavior is the same.) Notice how, right after append, the function still doesn’t exist — it only becomes callable once load fires:
But what if the fetch never succeeds? Maybe the file is gone (a 404), maybe the server is unreachable. In that case load never fires — and you need the other event.
script.onerror
A failed download shows up as an error event. Point a script at a URL that doesn’t exist and watch it fire:
let script = document.createElement('script');
script.src = "https://example.com/404.js"; // no such script
document.head.append(script);
script.onerror = function() {
alert("Error loading " + this.src); // Error loading https://example.com/404.js
};
One important limit: the error event tells you that the load failed, not why. You can’t read the HTTP status from it. A 404, a 500, a DNS failure, a dropped connection — they all surface as the same bare error event, with no status code attached. If you need the status, fetch is the right tool for the download instead.
The demo below makes that split concrete. The “Run clean code” button runs code that finishes without incident. The “Run buggy code” button runs code that calls a function that doesn’t exist — a runtime error that load/error would never surface. Only the global window.onerror handler hears about it:
Other resources
load and error aren’t special to scripts. They work for pretty much any element that pulls in an external file through a src — images, iframes, and more.
let img = document.createElement('img');
img.src = "https://example.com/media/skyline-poster.png"; // (*)
img.onload = function() {
alert(`Image loaded, size ${img.width}x${img.height}`);
};
img.onerror = function() {
alert("Error occurred while loading image");
};
Try both paths below. “Load a valid image” points at a real (inlined) picture and fires load; “Load a broken image” points at garbage data and fires error, at which point the handler swaps in a fallback — exactly the pattern you’d use to replace a 404’d image on a live page:
A couple of quirks are worth carrying with you:
- Most resources begin loading when they’re added to the document. An
<img>is the odd one out: it starts loading the moment it gets asrc, marked(*)above — not when it’s inserted into the page. So an image can be fully loaded before it ever appears in the DOM. - An
<iframe>firesloadno matter what. When an iframe finishes,iframe.onloadtriggers for both a successful page and a failed one. There’s no separateerrorpath for it.
Both behaviors are historical. They predate any consistent design and are kept for backward compatibility.
Cross-origin policy
There’s a browser security rule underneath all of this: a script running on one site can’t read the contents of another site. A script on https://facebook.com has no business reading your inbox at https://gmail.com, and the browser makes sure it can’t.
More precisely, the boundary is the origin — the triplet of protocol, domain, and port. Change any one of the three and you’re on a different origin with no automatic access across the line. A subdomain is a different origin. The same host on another port is a different origin. http:// versus https:// is a different origin.
That same wall applies to resources you load from other domains. If you pull in a script from another origin and it throws an error, the browser hides the details from you.
Take a script error.js that does nothing but call a function that doesn’t exist:
// 📁 error.js
missingHelper();
Load it from the same site it lives on, with a global handler watching:
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="/js/error.js"></script>
You get a full, useful report — message, file, and exact position:
Uncaught ReferenceError: missingHelper is not defined
https://example.com/js/error.js, 1:1
Now load the identical script from a different domain:
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="https://another-domain.example.com/js/error.js"></script>
The report collapses to nothing:
Script error.
, 0:0
The exact wording varies by browser, but the meaning is fixed: any detail about a cross-origin script’s internals — the message, the line, the stack trace — is stripped out. That’s the origin boundary doing its job.
Why care about those details? Error-tracking services — the kind that listen on window.onerror, store every error your real users hit, and give you a dashboard to dig through them — are only as good as the data they receive. You can build one yourself. But when the failing code came from another origin and you didn’t opt into sharing, all your monitor records is a useless Script error.
The same cross-origin restriction (CORS) covers other resource types too, not just scripts.
To lift the veil, two things have to line up: the <script> tag needs a crossorigin attribute, and the remote server has to send the right response headers. One without the other isn’t enough.
There are three levels of access:
- No
crossoriginattribute — access prohibited. This is the default, and the reason you sawScript error.above. crossorigin="anonymous"— access allowed if the server responds withAccess-Control-Allow-Originset to*or to your exact origin. The browser sends no cookies or HTTP auth to the remote server.crossorigin="use-credentials"— access allowed if the server sendsAccess-Control-Allow-Originwith your origin andAccess-Control-Allow-Credentials: true. The browser does send cookies and HTTP auth along.
In the failing example, there was no crossorigin attribute at all, so access was blocked. Add one and things change. If you don’t need cookies sent along, "anonymous" is the simplest choice — it needs just the one server header:
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script crossorigin="anonymous" src="https://another-domain.example.com/js/error.js"></script>
As long as the server sends back an Access-Control-Allow-Origin header, the browser now trusts the cross-origin relationship and hands you the full error report again.
crossorigin=“anonymous”
Access-Control-
Allow-Origin
Summary
Images, external stylesheets, scripts, and most other resources loaded through a src give you two events to track their download:
loadfires on a successful load.errorfires on a failed one.
Remember the distinctions that trip people up:
- An
<img>starts loading when it gets asrc, not when it’s added to the page. - An
<iframe>only ever firesload— for both success and failure — so you can’t useerrorto detect a broken iframe. load/errorreport on the download. A script that loads fine but throws at runtime still firesload; catch those runtime errors withwindow.onerror.- For cross-origin scripts, error details are hidden unless the tag carries
crossoriginand the server sends the matching CORS header.
The older readystatechange event works on resources as well, but you’ll rarely reach for it — load and error cover the same ground with far less ceremony.