JavaScript ES2025 & Beyond: New Features, Status, and Examples
JavaScript keeps evolving, and ES2025 (ECMAScript 2025) brings a wave of new features that make code more expressive, safer, and easier to write and maintain. Here’s a deeper look at the highlights:
Pattern Matching
Pattern matching is a new syntax in ES2025 that lets you check the shape and content of a value—like objects, arrays, numbers, or strings—and then run code based on what you find.
Think of it as a smarter, more powerful version of switch, with built-in destructuring.
Status: Stage 3 proposal (not yet in browsers or Node.js).
How Does Pattern Matching Work?
You use the match keyword, followed by the value you want to check (the input), and then a set of patterns (cases) to match against.
The first pattern that matches the input is executed.
Example 1: Matching Different Data Types
function handleInput(input) {
match (input) {
{ type: "user", name } => console.log(`User: ${name}`),
[first, ...rest] => console.log(`Array starts with ${first}`),
42 => console.log("The answer!"),
String => console.log(`A string: ${input}`),
_ => console.log("Unknown type")
}
}
handleInput({ type: "user", name: "Alice" }); // User: Alice
handleInput([10, 20, 30]); // Array starts with 10
handleInput(42); // The answer!
handleInput("hello"); // A string: hello
handleInput(true); // Unknown type
What’s happening?
- The input to
matchis checked against each pattern, top to bottom. { type: "user", name }matches any object with atypeproperty of"user"and extracts thename.[first, ...rest]matches arrays, extracting the first element.42matches the number 42.Stringmatches any string._is a catch-all for anything else.
Example 2: Matching Nested Objects
function handleApiResponse(response) {
match (response) {
{ status: "ok", payload: { id, value } } =>
console.log(`Success: id=${id}, value=${value}`),
{ status: "error", error } =>
console.error(`Error: ${error}`),
_ => console.warn("Unknown response")
}
}
handleApiResponse({ status: "ok", payload: { id: 1, value: 99 } }); // Success: id=1, value=99
handleApiResponse({ status: "error", error: "Not found" }); // Error: Not found
handleApiResponse({ foo: "bar" }); // Unknown response
Example 3: Matching Arrays with Specific Length
function describeArray(arr) {
match (arr) {
[a, b, c] => console.log(`Three elements: ${a}, ${b}, ${c}`),
[a, b] => console.log(`Two elements: ${a}, ${b}`),
[a] => console.log(`One element: ${a}`),
[] => console.log("Empty array"),
_ => console.log("Something else")
}
}
describeArray([1, 2, 3]); // Three elements: 1, 2, 3
describeArray([1, 2]); // Two elements: 1, 2
describeArray([1]); // One element: 1
describeArray([]); // Empty array
describeArray([1, 2, 3, 4]); // Something else
Key Takeaways
- The input to
matchcan be any value: object, array, number, string, etc. - Each pattern is checked in order. The first match "wins."
- Patterns can destructure objects and arrays, check for values, or use
_as a fallback. - Pattern matching is more expressive and less error-prone than long
if/elseorswitchstatements.
Tip:
Pattern matching is great for handling API responses, parsing data, or any situation where you need to branch logic based on the shape or content of your data.
2. Records and Tuples
Records and Tuples introduce deeply immutable data structures to JavaScript.
Status: Stage 2 proposal (not yet in browsers or Node.js).
- Record: Like an object, but immutable.
- Tuple: Like an array, but immutable.
Why is this important?
- Prevents accidental mutation of data.
- Makes functional programming safer and easier.
- Guarantees that data won’t change after creation.
Example:
const rec = #{ name: "Alice", age: 30 };
const tup = #[1, 2, 3];
console.log(rec.name); // Alice
rec.name = "Bob"; // Error! Records are immutable
tup[0] = 99; // Error! Tuples are immutable
Records and Tuples are deeply immutable—nested objects and arrays inside them are also frozen.
3. Async Context API
Async Context allows you to maintain and propagate context across asynchronous calls, such as requests, logging, or tracing.
Status: Stage 3 proposal (not yet in browsers or Node.js; similar APIs available in Node.js).
Why is Async Context needed?
In JavaScript, asynchronous operations like setTimeout, promises, or async/await can make it difficult to keep track of contextual information (such as user data, request IDs, or logging state).
Normally, if you set a variable before an async call, its value might be lost or overwritten by the time your callback runs, especially if multiple requests are handled in parallel.
Async Context solves this by:
- Attaching context to the current async flow.
- Automatically preserving and restoring that context across all async boundaries.
- Ensuring that, even inside deeply nested callbacks or after delays, you still have access to the correct context for that operation.
Note: This requires support from the environment, such as Node.js with Async Context enabled.
Example:
import { AsyncContext } from 'async-context';
const ctx = new AsyncContext();
ctx.run(() => {
ctx.set('userId', 42); // Set dynamically; could be any value, such as from a loop or request. 42 is just an example.
setTimeout(() => {
// Still inside the same context!
// ctx.get('userId') === 42
// Here we retrieve the value previously stored with `ctx.set`
logUserAction(ctx.get('userId'));
}, 100);
});
Why is this important in setTimeout?
Without Async Context, the code inside setTimeout might not know which user or request it belongs to, especially in a server handling many requests at once.
With Async Context, you can reliably access the correct context, making logging, tracing, and request-scoped data safe and predictable—even in asynchronous code.
Use cases:
- Request tracing in web servers
- Logging with request/session info
- Propagating user/session data in async code
4. AsyncLocalStorage (Node.js)
AsyncLocalStorage is a Node.js API (from the async_hooks module) that lets you store and access data scoped to the current asynchronous execution context.
It is similar in spirit to AsyncContext, but is available in Node.js (v13.10.0+) today.
Status: Available in Node.js v13.10.0+ (stable since v14).
Example:
import { AsyncLocalStorage } from 'async_hooks';
const storage = new AsyncLocalStorage();
function logUser() {
setTimeout(() => {
// Always shows the correct user for this async flow!
console.log("User:", storage.getStore());
}, 1000);
}
storage.run("Alex", () => {
logUser();
});
Why is this useful?
- Perfect for request tracing, logging, and passing user/session info in web servers.
- Prevents context loss in asynchronous code.
- Each async flow (like a request) gets its own context, making your code safer and easier to reason about.
5. LocalStorage (Browser)
localStorage is a browser API for storing key-value data persistently in the user's browser.
It is global and shared across all code running in the same browser origin.
Status: Available in all modern browsers (since HTML5).
Example:
localStorage.setItem('userId', 'Alex');
console.log(localStorage.getItem('userId')); // "Alex"
Any script on the same origin can read or write this value at any time.
Why is this useful?
- Store user preferences, tokens, or settings that persist across page reloads.
- Data is available to all scripts on the same origin.
Async Context vs AsyncLocalStorage vs LocalStorage
| Feature | AsyncContext (ES2025) | AsyncLocalStorage (Node.js) | localStorage (Browser) |
|---|---|---|---|
| Environment | All JS (future standard) | Node.js (server-side) | Browser (client-side) |
| Scope | Per async flow | Per async flow | Global (per browser origin) |
| Persistence | Temporary (lives during flow) | Temporary (lives during flow) | Persistent (survives reloads) |
| Use Case | Track data in async flows | Track data in async flows | Store user data/settings |
| Isolation | Isolated per async flow | Isolated per async flow | Shared by all code |
Summary:
- AsyncContext (ES2025) and AsyncLocalStorage (Node.js) are designed for tracking context in asynchronous code, keeping data isolated per async flow (like per request).
- localStorage is for persistent, global storage in the browser, not related to async context or isolation.
- They solve different problems and are not interchangeable.
6. Native JSON Modules
You can now import JSON files directly as modules, without extra loaders or hacks.
Status: Stage 3 proposal; available in Node.js v17+ and Chrome 91+ (with import ... assert { type: 'json' }).
Why is this useful?
- No need for custom loaders or
require()tricks. - Type-safe imports in TypeScript.
- Cleaner code for configuration, data, or localization.
Example:
import config from './config.json' assert { type: 'json' };
console.log(config.apiUrl);
Just import and use—no fuss.
7. Pipeline Operator (|>)
The pipeline operator makes chaining functions easier and more readable.
It lets you pass the result of one expression directly into the next function, left-to-right.
Status: Stage 2 proposal (not yet in browsers or Node.js).
Why is this useful?
- Avoids deeply nested function calls.
- Makes code read top-to-bottom, like a data flow.
- Easier to refactor and debug.
Example:
const result = "hello"
|> (x => x.toUpperCase())
|> (x => `Greeting: ${x}`);
console.log(result); // "Greeting: HELLO"
Compare with traditional nesting:
const result = `Greeting: ${("hello").toUpperCase()}`;
With pipelines, you can chain as many steps as you want, and each step is clear.
Advanced Example:
const final = user
|> validateUser
|> saveUser
|> sendWelcomeEmail;
Each function receives the output of the previous one.
Other Notable Features
Symbol.observable
Symbol.observable is a new well-known symbol in JavaScript that standardizes the way objects signal they are "observable."
This is especially useful for reactive programming, streams, and libraries like RxJS.
Status: Stage 1 proposal (not yet in browsers or Node.js; used by RxJS and other libraries internally).
What is an Observable?
An observable is an object that represents a stream of values over time.
You can subscribe to an observable to receive updates whenever new data is available.
Why Symbol.observable?
Before ES2025, different libraries used different ways to mark objects as observables.
Symbol.observable provides a standard method so any library or framework can recognize and work with observables in a consistent way.
How Does It Work?
An object is considered observable if it has a method keyed by Symbol.observable that returns itself (or another observable).
Example: Creating a Simple Observable
const myObservable = {
subscribe(observer) {
observer.next(1);
observer.next(2);
observer.complete();
},
[Symbol.observable]() {
// This method signals that this object is an observable
return this;
}
};
// Usage: A library or framework can check for Symbol.observable
if (typeof myObservable[Symbol.observable] === 'function') {
const obs = myObservable[Symbol.observable]();
obs.subscribe({
next: value => console.log('Value:', value),
complete: () => console.log('Done!')
});
}
// Output:
// Value: 1
// Value: 2
// Done!
Integration Example (with RxJS-like API)
function fromArray(arr) {
return {
subscribe(observer) {
arr.forEach(item => observer.next(item));
observer.complete();
},
[Symbol.observable]() {
return this;
}
};
}
const obs = fromArray([10, 20, 30]);
const observable = obs[Symbol.observable]();
observable.subscribe({
next: value => console.log('Stream:', value),
complete: () => console.log('Stream complete!')
});
// Output:
// Stream: 10
// Stream: 20
// Stream: 30
// Stream complete!
Why Is This Useful?
- Interoperability: Libraries can work together if they recognize
Symbol.observable. - Standardization: No more guessing if an object is observable—just check for the symbol.
- Framework Support: Frameworks like RxJS, Angular, and others can adopt this standard for better compatibility.
Array.prototype.groupBy
Groups array items by a key, making data aggregation much simpler.
Status: Stage 3 proposal; available in recent versions of Node.js (v20+) and some browsers (Chrome 118+).
Example:
const grouped = [{type: 'a'}, {type: 'b'}, {type: 'a'}].groupBy(x => x.type);
// { a: [{type: 'a'}, {type: 'a'}], b: [{type: 'b'}] }
No need for manual loops or reduce—just group by any property or function.
Set Methods: union, intersection, difference
Sets now have built-in methods for common set operations.
Status: Stage 3 proposal; available in Node.js v20+ and some browsers (Chrome 118+).
Example:
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);
const union = setA.union(setB); // Set {1, 2, 3, 4}
const intersection = setA.intersection(setB); // Set {2, 3}
const difference = setA.difference(setB); // Set {1}
These methods make mathematical set operations easy and readable.
Summary Table
| Feature | Description | Example Syntax |
|---|---|---|
| Pattern Matching | Match/destructure data structures | match (value) { ... } |
| Records & Tuples | Immutable objects/arrays | #{ ... }, #[ ... ] |
| Async Context | Context across async calls | AsyncContext.run(...) |
| AsyncLocalStorage | Node.js async context API | storage.run(...) |
| LocalStorage | Persistent browser storage | localStorage.getItem() |
| Native JSON Modules | Import JSON directly | import ... from ...json |
| Pipeline Operator | Chain functions with ` | >` |
| Array.groupBy | Group array items by key | arr.groupBy(fn) |
| Set Operations | Union, intersection, difference on sets | set1.union(set2) |
| Symbol.observable | Standardizes observable pattern | [Symbol.observable]() |
ES2025 brings more expressive, safer, and modern JavaScript.
Try these features as they land in browsers and Node.js!
Notes on TC39
- TC39 stands for "Technical Committee 39," which is the committee responsible for evolving the ECMAScript (JavaScript) language standard.
- The name comes from the ECMA(European Computer Manufacturers Association) International standards organization, where TC39 is the 39th technical committee.
- TC39 reviews, discusses, and advances proposals for new JavaScript features.
You can see all current proposals and their stages at github.com/tc39/proposals. - Proposals move through stages (0 to 4) before being added to the official language:
- Stage 0: Strawman (idea)
- Stage 1: Proposal (problem & solution)
- Stage 2: Draft (specification)
- Stage 3: Candidate (implementation & feedback)
- Stage 4: Finished (ready for inclusion in ECMAScript)
- TC39 meetings include browser vendors, Node.js maintainers, and other stakeholders to ensure JavaScript evolves in a way that benefits the whole
