JavaScript is the main client-side scripting language of web development. Each word of that description says something. It runs on the client, in the visitor's browser. Its focus is the web, although it has no such limit: the previous page shows it running in Node and elsewhere. And it is scripting, delivered as source text and run by the page that loads it. It makes up one side of the client-side triangle with HTML and CSS, and it reaches the other two through the document object model, or DOM.
The advice for this section is to get the language right first and then attack the details, so that you know where to place blame. This page breaks that advice on purpose. You cannot try the language without a page to run it in, so the language waits until the next page.
1. Hello world
Here is the smallest useful page with script in it. The browser parses the HTML from the top. When it reaches the script element it stops, runs what is inside, and continues.
document.write puts text into the stream the parser is reading, at the point where the script sits, and the parser then reads that text as HTML. It makes a good first example because nothing else is needed to see a result. It should stay a first example. MDN marks it as deprecated and strongly discourages it, and the rest of these pages use console.log and textContent instead. It has three behaviors you may meet in old code. Called after the page has loaded, it clears the whole document. Called from a deferred or async script, it is ignored and the browser logs a message. And since it parses its input as HTML, passing it anything a visitor typed opens the door to cross-site scripting. A script might call it, which is why the parser has to stop at a classic script. The rest of this page works through that problem.
HTML and JavaScript do not mix freely
The interplay between HTML and JavaScript can be tricky at first. Inside a script element the text is JavaScript, so an HTML tag there is a syntax error, and the whole script fails.
There are two ways to get the bold text you wanted. You can put the tags inside the string, so that they reach the parser as HTML, or you can put the script inside the tags.
The mixing can go wrong in the other direction as well. The HTML parser does not understand JavaScript, so it ends the script at the first </script> it sees, even one inside a string. A script that needs that text in a string writes it as <\/script>.
This playground has only an HTML panel, and its script runs as the preview is parsed, as it would in your own file. Change the message, then try the bad version.
2. JavaScript's silent failings
If you tried that last change, the message disappeared and the rest of the page looked fine. Nothing on the page said anything was wrong. Browsers give visitors minimal feedback that a script has failed. The only way to see the message is the JavaScript console in your dev tools. (In that playground, look in your browser's console rather than the playground's panel, which connects only after the HTML has been parsed.)
Silent errors were not always the rule. Older browsers put a warning icon in the corner of the status bar, and Internet Explorer could show a dialog for each error, giving the line, the character and the message, and asking whether to continue running scripts on the page. That went away because it confused the people using the page, who could do nothing about it. So here is a small experiment: browse the web for a while with your console open. You will find errors on sites you use every day, and many of those sites still appear to work.
A failure stops the script it happens in, at that line. The rest of that script does not run, while the page and any other scripts carry on.
So keep the console open whenever you work on script. If a page looks right and does nothing, read the console before you read your code. Errors and async covers reading a stack trace and catching errors.
3. Four ways to add script
There are four standard ways to include script in an HTML document:
- Within the
<script>element. - As a linked file, through the
srcattribute of the<script>element. - Within an HTML event handler attribute such as
onclick. - Through the pseudo-URL
javascript:syntax, referenced by a link.
Hello world used the first. The next two sections stay with it, because they apply to every script element. Section 6 covers the third and fourth, and section 7 returns to the second, which is the way most script should reach a page.
4. Generalized top-to-bottom execution
You can use as many script elements as you like, in both the head and the body, and they run one after another unless you direct otherwise. All classic scripts in a page also share one global scope, so a function defined in the head can be called from the body.
A sequence of alerts makes the order easy to feel, because alert stops everything until it is dismissed. Each script runs when the parser reaches it, and the parser goes no further until the script has finished.
The playground makes the same point without the dialogs. Each script counts the headings that exist at the moment it runs. The first finds one, and the count grows as the parser moves down the page, even though four headings are in the source from the start. A script can only see what the parser has already reached.
Now add network calls for scripts, which bring timing into it, and the order in which things happen can get more dangerous. Script's blocking nature has produced simple heuristics such as "move the script to the bottom". They do help, as you can observe, but they encourage guessing what will happen, and guessing is not recommended. The rest of this page replaces that guess with knowing what the parser is doing.
5. When script doesn't run
Script masking
You will sometimes see old pages that wrap script in comments. This is script masking, or script hiding.
A browser that did not recognize the script element would print its contents on the screen, so authors hid the code from it in an HTML comment. Browsers of that kind are long gone, and your goal is to separate code from markup anyway, so do not write it. It continues to run in a classic script because the language keeps <!-- as a kind of comment for the sake of old pages. A module rejects it with a SyntaxError. The type="text/javascript" on that sample is another leftover: JavaScript is the default, so you can leave it off.
The noscript element
The noscript element provides an alternative rendering for browsers that have script turned off.
A longer form tells the visitor what to do next. Here the script writes one message and the noscript holds the other, so each visitor gets one of them.
Note what noscript does not do. It shows only when scripting is off. If scripting is on and your script fails to download or throws an error, the visitor gets neither the script's work nor the fallback. The head lesson makes the same point: scripts fail for more people than those who turn them off.
The meta refresh trick
The noscript element can also be used to keep visitors without JavaScript out of a site or app altogether. Put a meta refresh inside a noscript in the head, and those visitors are redirected to an error page right away.
This has possible downsides. Redirects may be turned off along with JavaScript at certain security settings. It also isn't friendly to bots, since a crawler that runs no script follows the redirect and never sees the page. Whether that is a bad thing depends on whether you are applying it to a site or an app. Most apps should not be traversed by bots anyway. How much JavaScript? returns to whether a page should require script at all.
6. Attributes and pseudo-URLs
Event handler attributes
HTML defines a set of event handler attributes related to JavaScript events, such as onclick and onmouseover, and you can bind JavaScript statements to them. The statements run when the event happens to that element. Returning false from the attribute cancels what the browser would have done, so the link below does not go to the search page.
Event handler attributes are closely intertwined with the HTML, which has good news and bad news. The good news is that you can clearly see what is bound to which element. The bad news is that you have mixed your code deep into your markup. That goes against the idea of separation of concerns and can make maintenance difficult later on. A site with a strict Content Security Policy also has to forbid them, as they are inline script.
An attribute also isn't advanced event binding. It gives you one handler per event and no easy way to add other bindings or control the event itself. Script that uses addEventListener can add as many as it likes, and the event is generally more accessible there. This does raise a question. Do you want things to be able to bind in easily, without you as the developer being aware of them and accounting for them? Section 8 comes back to it.
Note the parentheses in onclick="someFunc()". The attribute's value is statements to run, so onclick="someFunc" names the function and never calls it, with no error. Events, step 1 compares the attribute, the element property and addEventListener in full.
JavaScript pseudo-URLs
The javascript: pseudo-URL also triggers script statements, when a link using it is followed.
You could once type such a URL directly in the browser's location box, for example javascript:alert(5+9). Browsers have restricted that, because attackers talked people into pasting code there. Firefox's bug report records the reasoning for its own change. What happens now varies by browser, so use the console for this kind of experiment.
Pseudo-URLs do not degrade well when script is unavailable, and they may confuse a user when some links load pages and others cause actions. They carry a trap too. If the last expression's value is a string, the browser replaces the page with that string as a new document. This is why old code is full of javascript:void(0), which guarantees a value of undefined. They may be blocked by a Content Security Policy as well. For an action, use a button with a listener. For a place to go, use a link with a URL. You will read pseudo-URLs in older "app" code, and they have one use left in bookmarklets, which are small scripts saved as bookmarks.
7. Linked scripts
Like linked style sheets, you can store JavaScript in a separate file and reference it. Use a .js file, and put only JavaScript in it, with no script tags. Store these files as you would images, in a common directory of your site such as /scripts. Linked scripts can be cached, so a visitor downloads them once across many pages, and they clean up your HTML documents.
Two details of the element catch people. The end tag is required, because HTML does not honor <script src="danger.js" />: the parser treats everything after it as script content, and the rest of your page disappears. And if an element has a src, any code written inside it is ignored.
Potential hazards
Linked scripts have problems under the obvious conditions of network failure or extreme slowness. Each one can also add a round trip. Unless you use them excessively, external files are still the preferred way to separate code from markup.
Linking to a script on someone else's server raises a larger question. That site now has a performance and security impact on you.
If that server is slow, a classic script in your head holds up your page. If it is compromised, or sold, its code runs in your page with full access to the page and its visitors' input. This has happened at scale. In 2024 the polyfill.io domain changed hands, and researchers reported that it was injecting malicious code into more than 100,000 sites that linked to it. You can guard against a changed file with Subresource Integrity: an integrity attribute holds a hash of the file you expect, along with crossorigin, and the browser refuses a file that does not match. Hosting the file yourself avoids the question.
8. Improving the linked script, one take at a time
Having JavaScript mixed in with our markup, as in that onclick on the button, can make things a bit messy and is generally bad practice. We should keep things separated. So take the attribute out and bind the handler from danger.js. This small job takes several tries, and each fixes something and exposes something else.
Take 1: bind from the file
Uh oh. The console reports a TypeError along the lines of "can't access property "onclick", button is null". Since the parser stops parsing the HTML to run our JavaScript at this point, we are trying to locate a button element that doesn't exist yet. This is section 4's lesson again, and the error is the kind that section 2 warned about, because the page looks fine and the button does nothing.
head runs before the parser has reached the body, so nothing in the body exists for it to find.You can reproduce this in one panel. The script looks for the button and reports what it found, and the button comes after it.
Take 2: move it to the bottom
Someone on Stack Overflow says that to avoid this issue you should always put your scripts right before the end of your body element, which seems to fix it.
That seems right, but it's not quite right. If the page is rather large and takes a while to load, the button could appear and be clickable before danger.js has even started to load. Backloading all your scripts can add up as your web app grows, so be mindful of the "uncanny valley", where your page has visually loaded but isn't functional. The name comes from robotics, for a face that is close to human and unsettling because of what is missing.
Press the buttons in this demo as soon as it appears. The product and its buttons paint at once, and the script at the end of the body takes five seconds. The panel at the top notes each press that nothing answered. When the script arrives, the same buttons start working, and nothing about their appearance changes. If the script never arrives, they never work, and the panel says so.
The response time that people expect from a press is the subject of RAIL, and Performance covers how to set numbers for it.
Take 3: wait for the window
If danger.js waits to touch the DOM until the entire window has loaded, we could technically load it as early as we like, and it will wait.
The error is gone. We are still not there. What happens if another included script does window.onload = function () { /* other stuff */ };? What if other included code has its own function alertTest() { }, or a variable of a similar name? The big challenge is that JavaScript shares a namespace with other included scripts. The possibilities are name collisions, event collisions, and misbehaving or even malicious code.
In this playground two scripts stand for two files. Each sets window.onload and each defines alertTest. Only the second of each survives, so the console shows one line, and no error appears anywhere to tell you the first was lost.
Take 4: a wrapper object and a listener
Two changes answer those collisions. Hanging our names from one wrapper object means that we occupy one global name in place of many. addEventListener adds a listener without replacing anyone else's.
The one remaining global can still collide. If a second script also says let UCSD, that second script fails with a SyntaxError and none of it runs. Programming in the large follows the namespace problem further.
Take 5: stop waiting for images
But when does the load event actually fire? It fires when the entire page has loaded, which includes all the dependent resources, stylesheets and images. Do we really need all of those images before our interactivity? Does our script depend on them? In most cases it needs the HTML and can ignore the rest.
If we wait only until the DOM has been processed, with the DOMContentLoaded event, we can skip waiting for the entire page to load. It fires when the HTML has been completely parsed and all deferred scripts have run. It doesn't wait for images, subframes or async scripts.
DOMContentLoaded marks the end of parsing, and load the end of everything the page asked for. On a page with many images, the gap is large.The event is fired at the document and bubbles to window, so you will see the listener on either. Now the script waits as long as it needs to and no longer. That lets us put it in the head, where the browser finds it early. Shoving script elements into the head has its own cost, though.
These scripts stop the parser while the browser waits for them. Nothing in the body has been read, so there is nothing to paint, and the visitor looks at a blank page until the slow server answers. Waiting for an event inside the file does nothing to help, because the wait happens before the file has even arrived.
Take 6: defer and async
Two attributes remedy this, defer and async. Both tell the browser to download the script while parsing continues. They differ in when it runs. The head lesson's timeline draws all four cases side by side.
deferdownloads the script while parsing continues, then runs it after the document is parsed, before theDOMContentLoadedevent. Deferred scripts run in the order they appear.asyncdownloads the script while parsing continues and runs it the moment it arrives, interrupting the parser, in whatever order the downloads finish.
So async all the things, and all your render-blocking woes are in the past? Well, not so fast. async runs things whenever possible, meaning that the order is unpredictable. What if script-3 depends on script-2?
The Load lab measures all of this. It loads the same three scripts as classic, defer, async and module, each in a new document, using delays you choose, and it logs when each one runs against parsing and DOMContentLoaded. Each trial also shows what is painted. A classic script in each trial's body holds the parser for the time you set, standing in for a long document. The route it uses belongs to the site's Worker, and the lab tells you if that route is missing.
With the delays it starts with, script-1 takes 1,500 ms, script-2 takes 200 ms, script-3 takes 800 ms and the page takes 1,000 ms to parse. These results came from running it, and your times will differ by a few milliseconds.
| Trial | Parser reaches the body | Parser reaches the end | Scripts run | DOMContentLoaded |
|---|---|---|---|---|
| classic | about 1,500 ms | about 1,500 ms | 1, 2, 3, all at about 1,500 ms | about 1,500 ms |
defer | at once | about 1,000 ms | 1, 2, 3, all at about 1,500 ms | about 1,500 ms, after the scripts |
async | at once | about 1,000 ms | 2 at about 200 ms, 3 at about 800 ms, 1 at about 1,500 ms | about 1,000 ms, before script-1 |
| module | at once | about 1,000 ms | 1, 2, 3, all at about 1,500 ms | about 1,500 ms, after the scripts |
Read the classic row closely. The body waited 1,500 ms, not the 2,500 ms that the delays add up to, because a preload scanner looks ahead while the parser is stopped and starts the other downloads early. It can fetch ahead, but the scripts still run in order and the parser still waits. In the defer row, script-2 arrived long before it ran and waited its turn. In the async row, script-1 ran after DOMContentLoaded.
That last result is a trap for Take 5's code. Suppose danger.js is loaded with async, as in the first sample of this take, and it arrives after parsing has finished. It adds a listener for an event that has already fired, the listener never runs, and the button stays dead with no error. If a script may run late, check document.readyState before listening. A deferred script has no such problem. It runs before the event, so its listener works, and since the document is already parsed, it doesn't need the listener at all.
One more, a modern take
The 2015 edition of JavaScript introduced modules, and HTML's module type of script loads them. A module has its own scope, so we no longer worry about polluting the global namespace.
With modules we can get rid of that wrapper object. A function or variable at the top of a module stays inside it, so two modules can each have an alertTest without meeting.
Modules are deferred by default, and the defer attribute has no effect on them, so the Load lab's module trial matches its defer trial. By the time this code runs, the document has been parsed and the button exists, so the listener for DOMContentLoaded is gone too. If you add async to a module, it behaves like the async trial: it runs as soon as it and its imports are ready, the button may not exist, and an event listener may be too late. For code that works with the page, leave async off.
Modules change some other things:
- They run in strict mode automatically, and
thisat the top level isundefined. - Their names are not global, so the
onclick="alertTest()"from section 7 fails with aReferenceError, and you cannot callalertTest()from the console. - They need a server. Opening the HTML from a
file://URL gives CORS errors, and a module from another origin must be sent with CORS headers. - They still share the page. Another script can read the same DOM, replace
window.onloador attach to your button.
import and export, which are the reason modules exist, are covered with a working demo in Programming in the large.
9. Which method is best
As you have probably learned by now in this course, the answer is It Depends™. You are the director of the production that is your web page. There is no one best way to film a scene in a movie, which is why each scene is filmed in its own way. Similarly, there is no one best way to load your script. You look at the tools and techniques available to you and decide which best fits the needs of what your JavaScript file requires.
The questions a director asks of a script are these. Does it need the DOM? Does anything depend on it, or does it depend on anything? Is the page useful without it, and for how long? This table turns the answers into a starting point.
| The script | A good starting point | Because |
|---|---|---|
| Your application code, working with the page | type="module" | Runs after parsing, in order, with its own scope. |
| A classic script that works with the page, or that others depend on | defer | Same timing and order, and it shares the global scope with other classic scripts. |
| Independent: depends on nothing and nothing depends on it, such as some analytics | async | Runs as soon as it can, and order does not count. |
| Must run before the page is first painted, such as setting a saved color theme | A small inline classic script in the head | It blocks, on purpose, and needs no download. Keep it tiny. |
| Depends on the images or other resources being finished | A listener for load | The one case that needs the later event. |
Whichever you pick, decide what the page does during the valley. Controls that work without script, such as links and forms, have no valley at all. A control that depends on script can wait to appear until it works. Those choices are the subject of How much JavaScript?
10. Multiple script tags
Developers commonly reference multiple JavaScript files separately. The value of this is questionable. Each request adds network cost, and classic files share the same namespace. HTTP/2 and later make many requests cheaper by sharing one connection, but each file is still fetched, and a chain of imports is discovered a step at a time.
The mantra for this is code for yourself, prep for delivery. You keep files small and separate because that suits people, and a build step prepares what suits the network. That step was once often a task runner like Gulp. Today it is more likely a bundler such as esbuild or Rollup, often through Vite. This course does not teach those tools, and the same idea appears in Programming in the large.
11. Summary
| Concept | Key points |
|---|---|
| Hello world | document.write writes into the parser's stream. Use it to get started and to read old code. It is deprecated, wipes a loaded page, and is ignored in deferred and async scripts. |
| Intermixture | An HTML tag inside a script is a syntax error. </script> ends a script even inside a string. |
| Silent failure | Visitors see nothing when script fails. An error stops that script at that line, and the page carries on. Keep the console open. |
| Four ways | Inline script, linked src, event handler attributes, and javascript: URLs. Prefer linked files, and read the other two only in older code. |
| Top to bottom | Classic scripts run in order as the parser reaches them, and each can see only what has been parsed. |
| No script | noscript shows only when scripting is off, not when a script fails. A meta refresh inside it redirects visitors, and crawlers. Script masking is history. |
| Remote scripts | The host has a performance and security impact on you. Use integrity, or host the file yourself. |
| Take 1, in the head | The button doesn't exist yet, so the lookup returns null. |
| Take 2, at the bottom | Works, and opens the uncanny valley: visible but not functional. |
Take 3, window.onload | Waits, but another script can overwrite it. Scripts share a namespace. |
| Take 4, wrapper and listener | One global name, and listeners that add up. load still waits for every image. |
Take 5, DOMContentLoaded | Waits only for parsing. The script in the head still blocks the parser while it downloads. |
Take 6, defer and async | Both download during parsing. defer runs after parsing, in order, before DOMContentLoaded. async runs on arrival, in no fixed order, and may miss DOMContentLoaded. |
| Modules | Deferred by default, strict, with their own scope. No wrapper object and no event listener needed. Their names are not global, and they need a server. |
| Choosing | It Depends™. You are the director. Start from what the script needs and what depends on it. |
| Delivery | Code for yourself, prep for delivery. |
