What is `import.meta` in an ES module, what does `import.meta.url` typically give you, and why does the same line throw a SyntaxError when the file is loaded as a classic script?
answer
- meta-property, like new.target
- one object per module
- host decides the contents
- the module's own URL
- module-only syntax, parse-time failure
basics
~20 simport.meta is a per-module object the host fills with metadata about the currently running module. In browsers and Node ESM its url property holds the module's own URL, useful for resolving sibling assets. It is module-only syntax, so a classic script rejects it at parse time.
solid answer
~40 s`import.meta` is dedicated syntax — a *meta-property*, like `new.target` — that evaluates to an ordinary object created fresh for each module and populated by the host environment. The one property both browsers and Node ESM provide is `import.meta.url`, a string holding the fully resolved URL of the module itself (a `file:` URL in Node). The everyday use is resolving something relative to the module rather than to the page or the process's working directory: `new URL('./data.json', import.meta.url)`. Hosts may add more — Node exposes `import.meta.dirname` and `import.meta.filename` in recent versions, and `import.meta.resolve` is available where the host provides it. The syntax is only legal in module code: a classic script or a file the runtime treats as non-module fails to parse, which is a compile-time SyntaxError, not something a `try`/`catch` around it can rescue.
code
javascript · 13 lines// widgets/chart.js loaded as an ES module
console.log(import.meta.url);
// browser: "https://example.com/widgets/chart.js"
// Node: "file:///srv/app/widgets/chart.js"
// resolve a sibling asset relative to THIS module, not the document
const dataUrl = new URL('./data.json', import.meta.url);
export async function loadData() {
const res = await fetch(dataUrl);
if (!res.ok) throw new Error(`data.json failed: ${res.status}`);
return res.json();
}go deeper
Recall that import.meta.url tells a module its own URL, that new URL('./x', import.meta.url) finds a sibling file, and that it works only in modules.
Explain that it is a meta-property, one object per module, populated by the host — and that using it in a classic script is a parse-time SyntaxError rather than a runtime error.
Show awareness of portability: only url is dependable across hosts, filesystem paths need a proper URL conversion, and code shared between module and script contexts must not contain the syntax at all.
Own the policy for host-dependent metadata across a codebase — where module-relative asset resolution is allowed, how dual-context code avoids the syntax, and why sniffing import.meta fields is a fragile portability boundary.
## A meta-property, not an object you can import `import.meta` is grammar, in the same family as `new.target`. There is no global named `import`, nothing to import, and no way to reach the same object by another name. Each module gets its own `import.meta` object, created once, distinct from every other module's. The language deliberately says almost nothing about its contents. ECMAScript defines that it is an ordinary extensible object with a `null` prototype and hands it to the host to populate. So the properties you find depend on where the code runs — that is the whole point of the design. ## `import.meta.url` The one property you can rely on across browsers and Node ESM is `url`: a string containing the fully resolved URL of the current module. ```js // served from https://example.com/app/widgets/chart.js console.log(import.meta.url); // "https://example.com/app/widgets/chart.js" ``` In Node running ESM, it is a `file:` URL such as `file:///home/app/widgets/chart.js`. Its main job is **resolving things relative to the module**. Relative URLs in a browser normally resolve against the *document's* URL, not the script's, so a module cannot use a bare relative path to find its own sibling asset. `import.meta.url` fixes that: ```js const dataUrl = new URL('./data.json', import.meta.url); const res = await fetch(dataUrl); ``` `new URL(relative, base)` is standard, so this works identically in both hosts, and it is the ESM answer to "where am I?" — modules have no `__dirname`. If you actually need a filesystem path in Node, convert rather than string-slicing the URL — a URL is percent-encoded and a path is not. ## Other host-provided properties Hosts add their own. Two worth knowing: - `import.meta.resolve(specifier)` — resolves a specifier against the current module and returns the resulting URL string, using the same resolution rules a real import would. Available where the host provides it; check the environment before relying on it. - Node exposes `import.meta.dirname` and `import.meta.filename` in recent versions, giving the directory and file path directly. Because the set is host-defined, treat anything beyond `url` as environment-specific and feature-detect it. ## Why a classic script throws `import.meta` is only permitted in module code. If the same file is loaded as a classic script — `<script src="...">` without `type="module"`, or a runtime treating the file as non-module — the parser rejects it before anything executes: ```html <!-- throws at parse time --> <script src="./thing.js"></script> <!-- fine --> <script type="module" src="./thing.js"></script> ``` The key consequence is *when* the failure happens. A SyntaxError is raised while parsing the whole file, so: - no statement in that file runs, not even the ones before the offending line; - wrapping it in `try`/`catch` does nothing, because the `try` block itself was never parsed successfully; - guarding it with `if (typeof import.meta !== 'undefined')` does not help either, since the mere presence of the syntax is what fails. If you need a file to work in both worlds, keep `import.meta` out of it entirely, or ship two builds. Note the contrast with dynamic `import()`, which *is* legal in classic scripts — the two pieces of syntax do not have the same availability. ```js // legal in a classic script import('./mod.js').then(ns => ns.init()); // SyntaxError in a classic script console.log(import.meta.url); ``` ## Interview-worthy points - It is per-module: two modules do not share one metadata object, and mutating your module's `import.meta` does not affect anyone else's. It is extensible, so you *can* add properties to it, but doing so as a hidden channel between code in the same module is a smell. - Its prototype is `null`, so it has no `hasOwnProperty` or `toString` inherited from `Object.prototype`. - Contents are host-defined by design; `url` is the portable one. - The failure mode in a non-module context is a parse-time error, which is much blunter than a runtime `undefined`.
- Why can't you guard `import.meta.url` with a runtime `typeof` check to make a file work as both a script and a module?Because the failure is at parse time, not run time. The parser rejects the `import.meta` syntax while reading the file, so the whole file fails before any statement — including your guard — executes, and no `try`/`catch` can intercept it. The only ways out are keeping the syntax out of the shared file entirely or shipping separate builds.
- You need a filesystem path in Node and only have `import.meta.url`. What is the safe conversion?Convert the URL rather than slicing the string: `import.meta.url` is percent-encoded and prefixed with `file://`, so paths containing spaces or non-ASCII characters break naive manipulation. Node's `url` module provides `fileURLToPath` for exactly this; recent Node versions also expose `import.meta.dirname` and `import.meta.filename` directly, which is simpler when you can rely on the version.
- Which parts of `import.meta` are guaranteed by the language itself?Very little. ECMAScript specifies only that each module gets its own extensible, null-prototype object, and delegates the contents to the host. `url` is the property both browsers and Node ESM provide, so it is the portable one; anything else — `resolve`, Node's `dirname` and `filename` — is host-specific and should be feature-detected rather than assumed.
saying these in an interview costs you the question
- Treats import.meta as a global available everywhere
- Says a try/catch can rescue import.meta in a script
- Assumes import.meta.url is a filesystem path
- Believes the language defines the full property set
- Uses string slicing to turn the URL into a path