On the horizon
Learn which JavaScript proposals are close: Temporal and using for ES2027, decorators, the pipeline operator, pattern matching, signals and more.
- 01Know what is comingSay what Temporal,
using, decorators, the pipeline operator, pattern matching and Signals are for. - 02Judge how close each one isRead a proposal's stage and decide whether to use it, try it with a compiler, or just follow it.
- 03Do the job todayWrite today's version of each idea:
try/finally, wrapper functions, named steps,switchand JSDoc.
On the horizon: proposals worth watching
Some JavaScript features are almost here, and others are still ideas. This lesson looks at the proposals people talk about most: what each one is for, how close it is, and how you do the same job today. The stages come from TC39's list of proposals in October 2026, and they change over time.
const proposals = [
{ name: "Temporal", stage: 4 },
{ name: "Decorators", stage: 2.7 },
{ name: "Pipeline operator", stage: 2 },
{ name: "Pattern matching", stage: 1 },
];
for (const { name, stage } of proposals) {
console.log(name + ": " + (stage >= 3 ? "try it now" : "watch it"));
}Lines 1 to 6 list four proposals with their stages. Lines 7 to 9 print advice for each one: try it now from stage 3, when browsers start building a proposal, and watch it before that.
So it prints Temporal: try it now, then watch it for decorators, the pipeline operator and pattern matching. The how JavaScript evolves lesson explains the stages.
try it now only for stage 3 and up.Read each row of the table as: "this proposal is at this stage, does this, and you can try it like this."
| The proposal | Stage | What it is for | How to try it today |
|---|---|---|---|
| Temporal | 4 | Dates and times without Date's surprises. | Built into the newest browsers, or a polyfill such as @js-temporal/polyfill. |
using | 4 | Cleans up a resource when its block ends. | Built into the newest browsers, and TypeScript 5.2 and later. |
import defer | 3 | Loads a module now, but runs it only when it is first used. | Babel's import-defer plugin. |
| Decorators | 2.7 | Wraps a class or method with extra behavior. | TypeScript 5.0 and later, or Babel's decorators plugin. |
| ShadowRealm | 2.7 | Runs code with its own fresh set of built-ins. | Not yet. Use an iframe or a worker for now. |
Pipeline |> | 2 | Writes steps in the order they happen. | Babel's pipeline-operator plugin, for experiments. |
| AsyncContext | 2 | Keeps a value across await without passing it along. | Node.js's AsyncLocalStorage does a similar job. |
| Structs | 2 | Objects with a fixed shape that threads can share. | Object.seal gives a fixed shape today. |
| Pattern matching | 1 | Compares a value against shapes, like a stronger switch. | Not yet. Use switch or if. |
| Signals | 1 | Values that tell other code when they change. | The signal-polyfill package, or a framework's own signals. |
| Type annotations | 1 | Types that the engine skips, like comments. | TypeScript, or JSDoc comments. |
Picture a restaurant. Some dishes are on the menu and you can order them today. Some are specials the kitchen is testing. Some recipes are still changing every week, and some are only notes in the chef's notebook. You wouldn't plan a party around a notebook idea, but you might ask for a taste of a special.
- In real life: Dishes on today's menu
- In JavaScript: Finished features, such as Temporal
- In real life: Specials the kitchen is testing
- In JavaScript: Stage 2.7 and 3 proposals
- In real life: Recipes the chef is still changing
- In JavaScript: Stage 2 proposals
- In real life: Ideas in the chef's notebook
- In JavaScript: Stage 1 proposals
A proposal's stage tells you how close it is. Use finished features, try stage 2.7 and 3 proposals with a compiler or polyfill, and only watch stage 1 and 2 ideas. For each one, there is a way to do the same job today.
Finished for ES2027: Temporal and using
Two big proposals reached stage 4 and are expected in ES2027. The newest browsers already have both.
Temporal is a new way to work with dates and times. It fixes many of Date's surprises, such as months that start at 0.
const day = new Date(2026, 9, 13);
console.log(day.getMonth());
day.setDate(day.getDate() + 30);
console.log(day.getMonth() + 1, day.getDate());Line 1 makes a date for 13 October 2026. Date counts months from 0, so October is written 9, and line 2 prints 9.
Line 3 adds 30 days by changing the day of the month, which rolls over into November. Line 4 adds 1 to the month to get a normal number, and prints 11 12.
const today = Temporal.PlainDate.from("2026-10-13");
const due = today.add({ days: 30 });
console.log(today.month, due.toString());
console.log(today.until(due).days);
console.log(Temporal.PlainDate.compare(today, due));Line 1 makes a PlainDate, a date with no time and no time zone. Line 2 adds 30 days and gives a new date; the first one doesn't change. Line 3 prints 10 2026-11-12: Temporal counts months from 1.
Line 4 asks how far apart they are, and prints 30. Line 5 compares them and prints -1, meaning the first comes before the second.
Date numbers months from 0, so October is 9. Temporal numbers them from 1, so October is 10, as on a calendar.The second finished proposal is explicit resource management, written using. A resource is something that must be cleaned up after use, such as an open file or a connection. Today you clean up with try and finally.
function openCup(name) {
console.log("open " + name);
return {
close() {
console.log("wash " + name);
},
};
}
const cup = openCup("tea");
try {
console.log("drink");
} finally {
cup.close();
}
console.log("done");Lines 1 to 8 make a pretend cup that must be washed. Line 9 opens it. Lines 10 to 14 drink from it, and finally washes it on line 13, even if something throws. It prints open tea, drink, wash tea, done.
function openCup(name) {
console.log("open " + name);
return {
[Symbol.dispose]() {
console.log("wash " + name);
},
};
}
{
using cup = openCup("tea");
console.log("drink");
}
console.log("done");Lines 4 to 6 give the cup a method named Symbol.dispose. Line 10 declares the cup with using, which ties it to the block on lines 9 to 12.
When the block ends, JavaScript calls Symbol.dispose by itself, so this prints the same four lines, with no finally to forget.
using on line 10 ties the cup to the block. When the block ends on line 12, JavaScript calls the cup's Symbol.dispose method, so wash tea prints before done.Deferred and source phase imports: stage 3
Two stage 3 proposals change how modules are imported. With import defer, a module is loaded at the start but runs only when your code first uses it. That speeds up page start when some code is rarely needed.
import defer * as charts from "./charts.js";
button.addEventListener("click", () => {
charts.draw();
});Line 1 loads charts.js but doesn't run it. Line 3, on the first click, uses charts, and only then does charts.js run. No browser runs this yet, so this box has no Run button.
Source phase imports let you import a module's compiled form without running it, mostly for WebAssembly.
import source pricesModule from "./prices.wasm";
const prices = await WebAssembly.instantiate(pricesModule);Line 1 imports the compiled WebAssembly module without starting it. Line 2 starts it when the code is ready.
Today, the closest tool is import(), which loads and runs a module only when you call it.
async function showCharts() {
const charts = await import("./charts.js");
console.log(charts.draw());
}
console.log("page ready");
showCharts();Lines 1 to 4 make a function that imports charts.js when called. Line 5 prints page ready first. Line 6 calls the function, so charts.js runs and prints charts.js runs, then line 3 prints chart drawn.
Decorators and ShadowRealm: stage 2.7
A decorator is a function you put in front of a class or method with @. It receives the method and can give back a new function to use instead, for example one that logs each call.
function logged(method, context) {
return function (...args) {
console.log("calling " + context.name);
return method.apply(this, args);
};
}
class Cart {
@logged
total(price) {
return price + 1;
}
}
console.log(new Cart().total(3));Lines 1 to 6 make logged. It receives the method and an object describing it, and returns a new function that prints the method's name and then calls the real method.
Line 8 puts @logged in front of total. Line 13 would print calling total, then 4. Browsers don't run decorators yet, but TypeScript and Babel turn them into plain JavaScript.
function logged(method, name) {
return function (...args) {
console.log("calling " + name);
return method.apply(this, args);
};
}
class Cart {
total(price) {
return price + 1;
}
}
Cart.prototype.total = logged(Cart.prototype.total, "total");
console.log(new Cart().total(3));Lines 1 to 6 make the same kind of wrapper. Line 12 replaces total on the class's prototype with the wrapped version. Line 13 prints calling total, then 4.
ShadowRealm, also at stage 2.7, runs code with its own fresh set of built-ins, such as its own Array, so code inside it can't change yours.
const realm = new ShadowRealm();
const double = realm.evaluate("(n) => n * 2");
console.log(double(3));Line 1 makes a new realm. Line 2 builds a function inside it from text. Line 3 calls it from your code and would print 6. Today, an iframe or a worker gives a similar separation.
The pipeline operator, AsyncContext and structs: stage 2
At stage 2, the committee has chosen a design, but it can still change a lot. The pipeline operator, |>, passes a value through steps in the order they happen. In the current draft, % stands for the value from the step before.
const label = prices
|> %.filter((p) => p < 5)
|> sum(%)
|> `Total: ${%}`;Line 1 starts with prices. Line 2 keeps the ones under 5, line 3 adds them up, and line 4 makes the label. You read it top to bottom, in the same order the work is done.
const prices = [3, 4, 5];
const sum = (list) => list.reduce((a, b) => a + b, 0);
console.log(`Total: ${sum(prices.filter((p) => p < 5))}`);
const cheap = prices.filter((p) => p < 5);
const total = sum(cheap);
console.log(`Total: ${total}`);Line 3 does all three steps in one nested call, which you read from the inside out, and prints Total: 7. Lines 4 and 5 give each step a name instead, so line 6 prints Total: 7 too, and reads in order.
AsyncContext keeps a value available across await without passing it to every function, for example the id of the order being handled.
const orderId = new AsyncContext.Variable();
orderId.run("order-7", async () => {
await brew();
console.log(orderId.get());
});Line 1 makes a variable. Line 2 runs some code with it set to "order-7". Even after the await on line 3, line 4 would still read order-7. Node.js has a similar tool today, AsyncLocalStorage.
Structs are objects with a fixed shape: you can't add properties later. Shared structs can also be used by several threads at once.
struct Point {
x;
y;
}
const point = new Point();
point.z = 1;Lines 1 to 4 declare a struct with two fields. Line 6 tries to add a third, which the proposal makes an error.
const point = Object.seal({ x: 0, y: 0 });
point.x = 3;
point.z = 1;
console.log(point.x, point.z);
console.log(Object.keys(point).join(" "));Line 1 seals an object: its properties can change, but none can be added. Line 2 works, and line 3 is silently ignored in sloppy code. Line 4 prints 3 undefined, and line 5 prints x y.
Pattern matching, Signals and type annotations: stage 1
At stage 1, the committee agrees the problem is worth solving, but the solution may change completely. Treat these as ideas to follow, not syntax to learn by heart.
Pattern matching compares a value against several shapes and picks the first that fits, like a stronger switch.
const message = match (status) {
when "paid": "Thanks!";
when "late": "Sorry for the wait";
default: "Waiting";
};Line 1 matches status. Lines 2 and 3 each check one value, and line 4 handles everything else. The whole match gives one value, which goes into message.
function messageFor(status) {
switch (status) {
case "paid":
return "Thanks!";
case "late":
return "Sorry for the wait";
default:
return "Waiting";
}
}
console.log(messageFor("late"), "/", messageFor("new"));Lines 1 to 10 return a message for each status. Line 11 prints Sorry for the wait / Waiting.
Signals are values that tell other code when they change. Frameworks such as Angular, Solid and Vue already use the idea. The proposal would give them one shared, built-in version. Here is a small model of the idea, written for this lesson.
function createState(value) {
const watchers = [];
return {
get: () => value,
set(next) {
value = next;
watchers.forEach((run) => run());
},
watch: (run) => watchers.push(run),
};
}
const price = createState(3);
price.watch(() => console.log("total is now " + (price.get() + 1)));
price.set(4);
price.set(5);Lines 1 to 11 make createState, which keeps a value and a list of watchers. Line 7 runs every watcher whenever the value is set.
Line 12 makes a price of 3, and line 13 adds a watcher that prints the total. Lines 14 and 15 change the price, so it prints total is now 5 and total is now 6.
A replay of the example code, not a view inside a JavaScript engine. createState is a small model of the idea behind Signals, not the proposal's own API.
script
const watchers = []; return { get: () => value, set(next) { value = next; watchers.forEach((run) => run()); }, watch: (run) => watchers.push(run), };}const price = createState(3);price.watch(() => console.log("total is now " + (price.get() + 1)));price.set(4);price.set(5);Press Step and watch the price beside line 12 change, and the watcher run each time. This replays the example code; it isn't a view inside the JavaScript engine.
Type annotations would let you write types in JavaScript files, like TypeScript. Engines would skip them, the way they skip comments. They would not check them.
function total(price: number, tax: number): number {
return price + tax;
}Line 1 says that price and tax are numbers, and so is the result. Today this is a SyntaxError.
/**
* @param {number} price
* @param {number} tax
* @returns {number}
*/
function total(price, tax) {
return price + tax;
}
console.log(total(3, 1));Lines 1 to 5 are a JSDoc comment that describes the same types. Editors and TypeScript read it, and engines skip it. Line 9 prints 4.
Trying proposals safely
Before you use a proposal, ask two questions: what stage is it at, and do the browsers your users have support it? The playground answers the second question for the browser you are using right now.
console.log(typeof Temporal === "undefined" ? "not built in" : "built in");checking…4checking…Checking this browser…
In a browser from 2026, Temporal and using show built in, and every other proposal shows not built in. In an older browser, all of them show not built in. The advice depends on the stage.
- Temporal
using cup = openCup()import defer * as charts from "./charts.js"@loggedon a methodnew ShadowRealm()prices |> sum(%)new AsyncContext.Variable()match (status) { … }new Signal.State(3)function total(price: number)
Put each proposal with its stage in October 2026: finished, stage 3 or 2.7, stage 2, or stage 1.
Some rules of thumb. Use a polyfill only for finished or stage 3 features. Use a compiler for stage 2.7 syntax if you can update your code when the proposal changes. For stage 1 and 2, try it in a small experiment, and read the proposal's repository to follow it.
Common mix-ups
Most mix-ups come from treating a proposal as finished because a tool already supports it.
- "Decorators are part of JavaScript." No. They are at stage 2.7. TypeScript and Babel turn them into plain JavaScript.
- "Type annotations will make JavaScript check types." No. Engines would skip them, like comments.
- "Temporal is a library you install." No. It is finished, and the newest browsers have it built in. Polyfills are for older browsers.
- "A stage 1 syntax is how it will look." No. It may change completely before it ships.
- "
import deferskips loading the module." No. It loads the module, but runs it only when it is first used. - "Signals are a framework." No. The proposal is a small built-in that frameworks could share.
The table below puts three of these side by side. Read each row as: "people often think this, but this is true."
| The proposal | What people think | What is true |
|---|---|---|
| Decorators | They are already part of JavaScript. | They are at stage 2.7. TypeScript and Babel compile them away. |
| Type annotations | JavaScript will start checking types. | The proposal makes engines skip types, not check them. |
| Temporal | It is a library you must install. | It is finished, and the newest browsers have it built in. |
Practice exercises
Each exercise uses today's tools for one of the ideas in this lesson. Guess first, then run it.
What does line 1 print?
console.log(new Date(2026, 0, 31).getMonth(), new Date(2026, 12, 1).getFullYear());console.log(new Date(2026, 0, 31).getMonth(), new Date(2026, 12, 1).getFullYear());January is month 0, and month 12 of 2026 is January 2027, so line 1 prints 0 2027. Temporal avoids both surprises.
What does line 7 print?
function advise(stage) {
if (stage === 4) return "use it";
if (stage >= 3) return "try it with a fallback";
if (stage >= 2.7) return "try it with a compiler";
return "watch it";
}
console.log(advise(2.7), "/", advise(2));function advise(stage) {
if (stage === 4) return "use it";
if (stage >= 3) return "try it with a fallback";
if (stage >= 2.7) return "try it with a compiler";
return "watch it";
}
console.log(advise(2.7), "/", advise(2));2.7 reaches the third check, so it says try it with a compiler. 2 reaches the last line, watch it. Line 7 prints both with a slash between.
Rewrite line 4 as named steps, in the order they happen. What does your program print?
const trim = (text) => text.trim();
const upper = (text) => text.toUpperCase();
const exclaim = (text) => text + "!";
console.log(exclaim(upper(trim(" tea "))));const trim = (text) => text.trim();
const upper = (text) => text.toUpperCase();
const exclaim = (text) => text + "!";
const trimmed = trim(" tea ");
const loud = upper(trimmed);
console.log(exclaim(loud));trimmed is "tea", loud is "TEA", and exclaim adds the !, so it prints TEA!, the same as the nested call.
What does the program print, in order?
function openCup() {
console.log("open");
return { close: () => console.log("wash") };
}
const cup = openCup();
try {
console.log("drink");
throw new Error("spilled");
} catch (error) {
console.log(error.message);
} finally {
cup.close();
}function openCup() {
console.log("open");
return { close: () => console.log("wash") };
}
const cup = openCup();
try {
console.log("drink");
throw new Error("spilled");
} catch (error) {
console.log(error.message);
} finally {
cup.close();
}It prints open, drink, spilled, then wash. finally washes the cup even though something threw, which is the job using will do for you.
Finish counted so that every call adds 1 to calls. What does your program print?
function counted(method) {
// Wrap method so every call adds 1 to calls.
return method;
}
let calls = 0;
class Cart {
total(price) {
return price + 1;
}
}
Cart.prototype.total = counted(Cart.prototype.total);
const cart = new Cart();
cart.total(3);
cart.total(4);
console.log("calls: " + calls);function counted(method) {
return function (...args) {
calls += 1;
return method.apply(this, args);
};
}
let calls = 0;
class Cart {
total(price) {
return price + 1;
}
}
Cart.prototype.total = counted(Cart.prototype.total);
const cart = new Cart();
cart.total(3);
cart.total(4);
console.log("calls: " + calls);The wrapper adds 1 on each call and passes the call on, so after two calls it prints calls: 2. With decorators, you would write @counted above total instead of the Cart.prototype.total = counted(…) line.
What does line 17 print?
function createState(value) {
const watchers = [];
return {
get: () => value,
set(next) {
value = next;
watchers.forEach((run) => run());
},
watch: (run) => watchers.push(run),
};
}
const cups = createState(1);
const log = [];
cups.watch(() => log.push("cups " + cups.get()));
cups.watch(() => log.push("price " + cups.get() * 3));
cups.set(2);
console.log(log.join(", "));function createState(value) {
const watchers = [];
return {
get: () => value,
set(next) {
value = next;
watchers.forEach((run) => run());
},
watch: (run) => watchers.push(run),
};
}
const cups = createState(1);
const log = [];
cups.watch(() => log.push("cups " + cups.get()));
cups.watch(() => log.push("price " + cups.get() * 3));
cups.set(2);
console.log(log.join(", "));Setting cups to 2 runs the first watcher, then the second, so the log is cups 2, price 6.
Check your understanding
For each question, ask what stage the proposal is at, and what today's way of doing the same job looks like. Each answer explains why it's right or wrong.
Question 1 of 7Which two proposals are finished and expected in ES2027?
Choose an answer to see the explanation.
Question 2 of 7What month number does Date give for October?
Read the code, then predictconsole.log(new Date(2026, 9, 13).getMonth());Choose an answer to see the explanation.
Question 3 of 7In what order does this try and finally code print?
Read the code, then predictfunction open() { console.log("open"); return { close: () => console.log("close") }; } const cup = open(); try { console.log("use"); } finally { cup.close(); }Choose an answer to see the explanation.
Question 4 of 7What does a method decorator receive?
Choose an answer to see the explanation.
Question 5 of 7What does stage 1 mean for pattern matching?
Choose an answer to see the explanation.
Question 6 of 7How often does this simple watcher run when the same value is set twice?
Read the code, then predictlet value = 3; const watchers = [() => console.log("now " + value)]; function set(next) { value = next; watchers.forEach((run) => run()); } set(4); set(4);Choose an answer to see the explanation.
Question 7 of 7Should you ship stage 2 syntax to users through a compiler?
Choose an answer to see the explanation.
Key takeaways
New features keep coming. Knowing each one's stage tells you whether to use it, try it, or just watch it.
- Temporal and
usingare finished and expected in ES2027; the newest browsers have both. import deferand source phase imports are at stage 3.- Decorators and ShadowRealm are at stage 2.7. TypeScript and Babel can compile decorators today.
- The pipeline operator, AsyncContext and structs are at stage 2.
- Pattern matching, Signals and type annotations are at stage 1, and may change completely.
- Every idea has a way to do the job today:
finally, wrappers, named steps,switchand JSDoc.
Remember the one-liner.
Use what is finished, try what is close with a compiler, and watch the rest.
Coming next: How new syntax is downleveled. It shows how tools like Babel turn tomorrow's syntax into code that today's engines can run.