Test plan for deferred re-exports (`export defer`)
@nicolo-ribaudo is already working on this.
Since Apr 7, 2026.
- Dominant language
- JavaScript
- Stars
- 2.8k
- Forks
- 564
- Avg merge
- 3d 7h
- Merged PRs (30d)
- 10
Description
Proposal: https://github.com/tc39/proposal-deferred-reexports
Spec text: https://tc39.es/proposal-deferred-reexports/
Feature flag: deferred-reexports
Stage: 2
Champions: @nicolo-ribaudo, @caiolima
Syntax (WIP - https://github.com/tc39/test262/pull/5033)
Valid syntax:
-
export defer { x } from "mod"-- single named deferred re-export -
export defer { x, y } from "mod"-- multiple named deferred re-exports -
export defer { x as y } from "mod"-- renamed deferred re-export -
export defer * as ns from "mod"-- deferred namespace re-export -
export defer { x } from "mod" with { type: "json" }-- with import attributes -
export defer { x as default } from "mod"
Early errors (SyntaxError):
-
export defer * from "mod"-- bare star withoutasis aSyntaxError -
export defer { x }-- missingfromclause (must be a re-export) -
export defer function f() {}-- cannot defer local declarations -
export defer default x-- cannot defer default export -
export defer const x = 1-- cannot defer variable declarations -
export deferused outside of a module (script context)
Load and Evaluation (WIP - https://github.com/tc39/test262/pull/5034)
-
export defer { x } from "./dep.js"where no consumer importsx--dep.jsis never loaded and evaluated -
export defer { x } from "./dep.js"where a consumer importsx--dep.jsis loaded and evaluated - Chained
export defer: A hasexport defer { x } from "./B.js", B hasexport defer { x } from "./C.js"-- C is only evaluated whenxis actually imported/accessed by the consumer of A -
export defer { x } from "./dep.js"where a consumer re-exportsxwithout deferexport {x} from ...and there IS a consumer ofxon entrypoint. It should trigger load and evaluation ofdep.js -
export defer { x } from "./dep.js"where a consumer re-exportsxwithout deferexport {x} from ...and there IS NO consumer ofxon entrypoint. It should trigger load and evaluation ofdep.js -
export * from "x", where x has a deferred non-default re-export. It should eagerly evaluate deferred re-exports -
export * from "x", where x has a deferred default re-export. It should not eagerly evaluate deferred re-exports, sinceexport * from "x"means re-exportsALL-BUT-DEFAULT.
Namespace Object (WIP)
Triggering evaluation via import * as ns: (WIP - https://github.com/tc39/test262/pull/5035)
-
import * as ns from "./reexport.js"where reexport hasexport defer { x } from "./dep.js". Ir eargerly loadsdep.js, and accessingns.xtriggers evaluation ofdep.jsand returns the correct value - Accessing a deferred export a second time does not re-trigger evaluation (module is cached). It returns same value
- Transitive:
import * as ns from "./reexport.js"where reexport hasexport defer { x } from "./middle.js"and middle hasexport defer { x } from "./deep.js". Accessingns.xtriggers evaluation through the chain - Acessing a deferred export that is not ready for sync execution throws a
TypeError -
import * as ns from "./reexport.js", where reexport hasexport defer { x } from "./dep.js", anddephas syntax error, it eagerly throwsSyntaxError, sincedep.jswill be loaded - Transitive:
import * as ns from "./reexport.js", where reexport has transitive export defer chain and source has syntax error, it eagerly throwsSyntaxError, sincesource.jswill be loaded
Property enumeration: (WIP - https://github.com/tc39/test262/pull/5038)
-
Reflect.ownKeys(ns)includes names fromexport deferdeclarations if it comes fromimport * as ns ... -
Reflect.ownKeys(ns)includes names fromexport deferdeclarations if it comes fromimport defer * as ns ... -
Reflect.ownKeys(ns)includes names fromexport deferdeclarations if it comes fromimport("mod") -
Reflect.ownKeys(ns)includes names fromexport deferdeclarations if it comes fromimport.defer("mod")
Interaction with import defer * as ns: (WIP - https://github.com/tc39/test262/pull/5039)
-
import defer * as ns from "./reexport.js"where reexport usesexport defer { x } from "./dep.js"-- accessingns.xtriggers evaluation of both the reexport and the deferred dependency, returns the correct value -
import defer * as ns from "./reexport.js"-- accessing a non-deferred binding onnstriggers evaluation of the reexport but NOT evaluation of deferred dependencies -
import defer * as ns from "./reexport.js"where reexport hasexport defer--Reflect.ownKeys(ns)triggers reexport evaluation but does NOT trigger evaluation of deferred modules -
Load test variants that we have on
import * as ns ...
Dynamic import (WIP - https://github.com/tc39/test262/pull/5040)
-
import("./reexport.js")where reexport usesexport deferdoesn't trigger evaluation of deferred exports (it's similar toimport * as ns from "reexport.js"). The evaluation happens once the deferred re-export is acessed. -
import("./reexport.js")where reexport usesexport defer {x} from "dep.js", and dep hasSyntaxError, it will result inSyntaxError, sincedep.jswill be loaded -
import.defer("./reexport.js")where reexport usesexport deferdoesn't trigger evaluation of deferred exports (it's similar toimport defer * as ns from "reexport.js"). The evaluation happens once the deferred re-export is acessed, and both the deferred module and the chain from deferred re-exports gets evaluated -
import.defer("./reexport.js")where reexport usesexport defer {x} from "dep.js", and dep hasSyntaxError, it will result inSyntaxError, sincedep.jswill be loaded -
export defer * as ns from "./dep.js"-- deferred namespace re-export works correctly
Evaluation Order
Basic ordering (non-deferred before deferred, deferred at end):
- Non-deferred exports evaluate before the re-exporting module's own code; deferred exports evaluate after
- Given a barre file
export { b } from "./b.js"; export defer { a } from "./a.js";and a entrypoint that importsaandb, the order should be:b.js-> barrel ->a.js-> entrypoint - Deferred exports evaluate in declaration order
-
export defer { x } from "./async-dep.js"whereasync-dep.jsuses top-level await -
export defer { x } from "./dep.js"combined withexport { y } from "./dep.js"for the same module but different bindings - Cycle through ancestor:
aimports{x}fromb,bhasexport defer {x} from "c",cexportsxand importsd,dimportsa. Expected evaluation order:b->d->c->a - Cycle back to defer source:
aimports{x}fromb,bhasexport defer {x} from "c",cexportsxand importsd,dimportsb. Expected evaluation order:b->d->c->a - Multiple deferred re-exports from different sources with interleaved non-deferred imports -- verify declaration order is respected across the full mix
Error Propagation
Syntax/resolution errors in deferred source modules:
- If source has
SyntaxError, but its reexport is never used, the program executes properly, because it will never be loaded - If source has
SyntaxError, and its reexported is used, the program throwsSyntaxError - If source has
SyntaxError, and its reexport is imported via a namespace object, the program throwsSyntaxErrorbecause source will be loaded
Runtime errors on source of deferred re-export:
- If it throws during evaluation, and it's never imported, then the program never throws
- If it throws during evaluation, and it's used, the evaluation will throw
- If it throws during evaluation, and it is imported as namespace, it will throw on access. Subsequent access will throw the same error
- If there are 2 differrent re-exports for
x, and they are imported as 2 different namespace objectsimport * ns1 from "reexport1"; import * ns2 from "reexport2";and there's an exception thrown on evaluation, both accessns1.xandns2.xshould throw the same exception.
Circular references:
-
export deferforming a cycle with its consumer throws aSyntaxError - Cyclic re-exports throws a
SyntaxErrorif it's imported - Cyclic re-exports never throws a
SyntaxErrorif it IS NOT imported, since it's never loaded.
Interaction with Other Features
With top-level await (async modules):
- A deferred module with an async dependency -- the async dependencies are gathered during loading and are eagerly evaluated if it's imported by entrypoint
- A deferred module with an async dependency -- the async dependencies are ignored during loading if it IS NOT imported by entrypoint
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.