Skip to main content

What is the difference between async and defer?

async vs defer - two HTML script attributes that both download scripts in parallel with HTML parsing, but differ in when execution happens: async runs the moment the download finishes, defer waits until the HTML parser is done.

Theory

TL;DR

  • async is like ordering food and eating it the second it arrives, no matter what else is happening at the table
  • defer is like finishing your main course first, then eating dessert in the order you ordered it
  • Main difference: async can interrupt HTML parsing to execute; defer never does
  • Use defer for scripts that touch the DOM or depend on other scripts. Use async for independent third-party scripts like analytics

Quick example

html
<!-- Blocks HTML parsing: downloads AND executes before parser continues --> <script src="app.js"></script> <!-- Downloads in parallel, executes immediately when ready (may interrupt parsing) --> <script src="analytics.js" async></script> <!-- Downloads in parallel, executes after HTML parsing, order preserved --> <script src="jquery.js" defer></script> <script src="app.js" defer></script>

Both async and defer start downloading immediately and let the HTML parser keep working. The difference shows up at execution time.

Key difference

async executes the moment the script finishes downloading. If parsing is still in progress, the browser pauses it, runs the script, then resumes. Two async scripts can execute in any order depending on which downloads faster. defer queues the script and only runs it after the HTML parser finishes, in document order. That makes defer predictable for anything that depends on other scripts or on the DOM existing.

When to use

  • defer - scripts that manipulate the DOM, depend on other scripts (React bundles, jQuery plugins, framework init code, analytics that reads page content)
  • async - third-party scripts that don't depend on your code and don't need the DOM (Google Analytics, Sentry, ad networks, chat widgets)
  • Neither - inline scripts (defer is ignored on inline code), or ES modules (type="module" is already deferred by spec)

Comparison table

AspectDefault <script>asyncdefer
DownloadBlocks parsingParallelParallel
ExecutionBlocks parsingImmediate (may interrupt)After parsing
Order preservedN/ANoYes
DOM ready at executionNoNoYes
Best forCritical inline codeIndependent third-partyApp code, dependencies
When to useRareAnalytics, ads, tracking95% of your scripts

How the browser handles this

When the HTML parser hits a <script> tag, it enters a decision point. With no attribute, it stops, downloads, executes, then continues. With async, it spawns a network request and keeps parsing, but the moment that script arrives, parsing stops and the script executes. With defer, it spawns the same parallel request but only executes after the parser finishes, right before the DOMContentLoaded event fires. Deferred scripts run in document order; async scripts run in download-completion order, which is non-deterministic.

One thing I have seen trip up teams: they assume async is always faster. For independent scripts, it is. But if your script needs the DOM, async can execute it before the expected elements exist. You get runtime errors that appear and disappear based on network speed, which makes them frustrating to reproduce.

Common mistakes

Mistake 1: Using async for dependent scripts

html
<!-- Wrong: jquery-plugin.js might execute before jquery.js --> <script src="jquery.js" async></script> <script src="jquery-plugin.js" async></script> <!-- Fix: defer preserves order --> <script src="jquery.js" defer></script> <script src="jquery-plugin.js" defer></script>

If jquery-plugin.js downloads faster, it executes first. You get $ is not defined at runtime, and the bug disappears on slower connections. Hard to reproduce, harder to explain to a client.

Mistake 2: Both async and defer on the same script

html
<!-- Wrong: async takes precedence, defer is silently ignored --> <script src="app.js" async defer></script> <!-- Fix: pick one --> <script src="app.js" defer></script>

The browser treats async defer as just async. This combination was a fallback for very old browsers that did not support async, but those browsers are gone. Avoid it today.

Mistake 3: Adding defer to inline scripts

html
<!-- Wrong: defer has no effect on inline scripts --> <script defer> console.log(document.getElementById('root')); // Still null </script> <!-- Fix: move to an external file --> <script src="app.js" defer></script>

The spec says defer only applies to external scripts with a src attribute. Inline scripts ignore it entirely and execute immediately.

Mistake 4: Adding defer to module scripts

html
<!-- Redundant: type="module" is already deferred by spec --> <script type="module" src="app.js" defer></script> <!-- Same behavior, defer attribute unnecessary --> <script type="module" src="app.js"></script>

Mistake 5: Expecting DOMContentLoaded to fire inside a deferred script

Deferred scripts run before DOMContentLoaded fires, not after. The DOM is ready, but the event has not been dispatched yet. If your deferred script listens for DOMContentLoaded, that handler will never fire because the event already passed by the time the listener registers.

Real-world usage

  • React / Vue / Angular bundles - defer (Next.js, Nuxt, Angular CLI all default to this)
  • Google Analytics - async (independent, does not need the DOM)
  • Sentry error tracking - async (captures errors independently)
  • jQuery + plugins - defer on both (preserves load order)
  • Stripe / PayPal checkout - async (third-party, self-contained)
  • Custom analytics that reads page content - defer (needs DOM to exist)

Follow-up questions

Q: What happens if a deferred script throws an error?
A: That script stops executing, but subsequent deferred scripts still run. The page does not break by itself, but any script that depended on the errored one may fail silently.

Q: What is the difference between defer and putting scripts at the end of </body>?
A: Functionally similar for execution order. But defer in <head> starts downloading earlier, during HTML parsing, so the script is ready sooner. Putting scripts at the end of body was the old workaround before defer was well-supported. Today, defer in <head> is the correct pattern.

Q: Can you use async with type="module"?
A: Yes. But modules are already deferred by default, so adding async makes them execute immediately on download like classic async scripts. That breaks the dependency order modules normally preserve. Rarely useful in practice.

Q: You have three scripts: A depends on B, B is independent, C depends on A. How do you load them optimally?
A: Load B with async since it has no dependencies. Load A and C with defer to guarantee A runs before C. B downloads in parallel and executes whenever ready; A and C execute in document order after parsing. If B is actually required by A, use defer for all three to be safe.

Examples

Basic: async interrupting DOM access

html
<head> <script src="analytics.js" async></script> </head> <body> <div id="app">Loading...</div> </body>
javascript
// analytics.js // This might run before <div id="app"> is parsed const app = document.getElementById('app'); console.log(app); // null if HTML is not fully parsed yet

analytics.js starts downloading the moment the browser sees the <script> tag. On a fast connection it can finish downloading before the body is parsed, so getElementById returns null. Switching to defer makes the read safe because execution waits for parsing to finish.

Intermediate: React initialization with defer

html
<head> <script src="react.js" defer></script> <script src="react-dom.js" defer></script> <script src="app.js" defer></script> </head> <body> <div id="root"></div> </body>
javascript
// app.js - runs after HTML parsing, after react.js and react-dom.js const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<App />);

All three scripts download in parallel during HTML parsing. They execute in order: react.js first, then react-dom.js, then app.js. By the time app.js runs, ReactDOM is available and #root exists in the DOM. This is exactly what Next.js generates by default for every page bundle.

Advanced: Race condition with async

javascript
// The bug: two async scripts with a dependency // <script src="jquery.js" async></script> // <script src="jquery-plugin.js" async></script> // Timeline on a fast CDN connection: // t=0ms: both scripts start downloading // t=40ms: jquery-plugin.js arrives (smaller file) -> executes -> ERROR: $ is not defined // t=80ms: jquery.js arrives -> executes, but plugin already failed // Timeline on a slow connection: // t=0ms: both scripts start downloading // t=200ms: jquery.js arrives first due to network reordering -> executes // t=250ms: jquery-plugin.js arrives -> $ exists -> works perfectly // The bug only appears on fast connections. Passes local testing, breaks on CDN. // Fix: use defer to guarantee order // <script src="jquery.js" defer></script> // <script src="jquery-plugin.js" defer></script> // Analytics can still be async because it has no dependencies // <script src="google-analytics.js" async></script>

This is the kind of bug that passes every test locally and breaks in production at scale. Async execution order depends on which file bytes arrive first, which changes based on network conditions, CDN edge location, and file size. defer removes that variable entirely.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?