DEV Community

Alex Cloudstar
Alex Cloudstar

Posted on • Originally published at alexcloudstar.com

ES2026 Is Here: The JavaScript Features That Actually Change How You Write Code

JavaScript has been my daily language for over a decade. Most ECMAScript releases in that time have felt incremental: a new array method here, a syntactic convenience there. Nice things, but not things that change how you think about writing code.

ES2026 is different.

The Temporal API alone is the biggest quality-of-life improvement to JavaScript dates since, well, since there has been JavaScript. The explicit resource management additions clean up a pattern that has been messy since async/await shipped. Error.isError fixes a subtle footgun that has caused real bugs in real production code. These are not incremental additions. They are fixes to problems that have generated Stack Overflow answers, npm packages, and silent bugs for years.

Here is a complete walkthrough of what shipped, why each thing matters, and how to start using it.


The Temporal API: Finally Replacing Date

Let me be direct about something. The JavaScript Date object is broken in ways that are not fixable without breaking the web. The community has known this for years. That is why libraries like date-fns, Luxon, and Day.js exist and why almost every non-trivial application reaches for one of them within the first sprint.

The Temporal API reached Stage 4 in March 2026, which means it is officially part of the ES2026 spec. It is a ground-up redesign of date and time handling that fixes the problems everyone has been working around.

What was wrong with Date

The original Date object is a Unix timestamp wrapper that exposes calendar representations inconsistently. The problems are well-documented but worth naming:

  • Date is mutable. Every operation that should return a new date actually modifies the original, which is a source of subtle state bugs.
  • Time zone handling is implicit and confusing. new Date() gives you a timestamp with the local time zone baked in, but many methods behave differently depending on whether you call the UTC or local variant.
  • Months are zero-indexed (January is 0, December is 11) but days start at 1. This has generated bugs in production code since 1995.
  • Parsing a date string with new Date("2026-04-13") gives you midnight UTC, but new Date("04/13/2026") gives you midnight local time. The inconsistency is not documented, just known.

Temporal fixes all of this. Temporal objects are immutable. Time zones are explicit and first-class. Arithmetic returns new objects. Parsing is unambiguous.

The core Temporal types

The most important shift in Temporal is that it separates types that have been conflated in Date.

Temporal.PlainDate represents a calendar date with no time and no time zone. This is what you want when you are working with a birthday, a due date, a billing cycle anchor, or any value where the time of day is irrelevant. There is no accidental UTC conversion to corrupt it.

Temporal.PlainTime represents a clock time with no date and no time zone. A store's closing time, a recurring alarm, a scheduled task within a day.

Temporal.PlainDateTime is a date and time with no time zone. This is for values that need a date and a time but where the time zone will be applied or inferred separately.

Temporal.ZonedDateTime is the full package: a date, a time, and a time zone. This is what you use when you are storing the exact moment something happened in a way that needs to be displayed in the user's local time.

Temporal.Instant is a point in time with no calendar context. An absolute timestamp. This replaces the most common use of Date in backend code.

What the API looks like

// No more new Date() footguns
const today = Temporal.Now.plainDateISO();
const tomorrow = today.add({ days: 1 });
const nextMonth = today.add({ months: 1 });

// Explicit time zones
const meeting = Temporal.ZonedDateTime.from({
  year: 2026,
  month: 4,
  day: 15,
  hour: 14,
  minute: 0,
  timeZone: 'America/New_York',
});

// Comparison that actually works
const isBefore = Temporal.ZonedDateTime.compare(meeting, other) < 0;

// Duration arithmetic
const duration = Temporal.Duration.from({ hours: 2, minutes: 30 });
const endTime = meeting.add(duration);
Enter fullscreen mode Exit fullscreen mode

No mutation. No zero-indexed months. No implicit UTC conversions.

Should you drop date-fns immediately?

Not immediately. Browser support for Temporal is shipping across major browsers in 2026, but adoption will take time to reach the point where you can ship without a polyfill. The @js-temporal/polyfill package works well if you need it today.

For new projects, Temporal is the right default. For existing projects heavily invested in date-fns or Luxon, migration is worth planning but not urgent. Both libraries are aware of Temporal and have stated plans around compatibility.

The era of reaching for a date library as the first dependency in every project is ending.


Explicit Resource Management: using and await using

This is the addition that surprised me most when I started using it. It sounds minor. It solves a problem you have probably written ten different try/finally blocks to work around.

The problem is that resources that need cleanup when you are done with them, things like database connections, file handles, network streams, timers, have always required manual teardown. The idiomatic pattern was:

// The old way
const connection = await db.connect();
try {
  const result = await connection.query('SELECT * FROM users');
  return result;
} finally {
  await connection.close();
}
Enter fullscreen mode Exit fullscreen mode

This works. It is also boilerplate that you have to remember to write every time, and that you can accidentally leave out when you are in a hurry or when early returns make the control flow complicated.

ES2026 adds a using declaration that automatically calls the object's [Symbol.dispose]() method when the block exits, regardless of whether it exits normally or throws.

Synchronous cleanup with using

// using for synchronous resources
{
  using handle = openFileHandle('data.txt');
  const content = handle.read();
  // handle[Symbol.dispose]() is called automatically here
}
Enter fullscreen mode Exit fullscreen mode

Any object that implements [Symbol.dispose]() works with using. The method gets called when the variable goes out of scope, whether the block exits normally, returns early, or throws.

Async cleanup with await using

For resources that require async cleanup, the await using syntax calls [Symbol.asyncDispose]() and awaits the result:

async function processData() {
  await using connection = await db.connect();
  const rows = await connection.query('SELECT * FROM users');
  return rows;
  // await connection[Symbol.asyncDispose]() is called automatically
}
Enter fullscreen mode Exit fullscreen mode

No try/finally. No risk of forgetting to close the connection. The disposal is guaranteed.

What objects support this?

The built-in runtimes are adding [Symbol.dispose] to things like ReadableStreamDefaultReader and ReadableStreamBYOBReader. Node.js is adding it to things like file handles. Any library that exposes resources you need to clean up can implement this protocol.

For your own code, adding the protocol is straightforward:

class DatabaseConnection {
  async query(sql: string) { /* ... */ }

  async [Symbol.asyncDispose]() {
    await this.close();
  }
}
Enter fullscreen mode Exit fullscreen mode

This is one of those features that will change how libraries are designed, not just how applications are written. Expect to see [Symbol.dispose] on database clients, HTTP clients, file system APIs, and test fixtures over the next year.


Error.isError(): Fixing a Subtle Footgun

This one is small. It is also fixing a real category of bug.

Here is the problem. You have a catch block. You want to check if the caught value is actually an Error object before accessing properties like .message or .stack. The natural thing to write is:

try {
  doSomething();
} catch (e) {
  if (e instanceof Error) {
    console.error(e.message);
  }
}
Enter fullscreen mode Exit fullscreen mode

This works in most cases. It breaks in two specific situations that come up more than they should.

First, when the error was thrown in a different realm. Iframes, workers, and some module contexts have their own global scope, which means their own Error constructor. An Error thrown in an iframe is not instanceof the Error in the main frame. They are different classes with the same name.

Second, when someone has been creative with what they throw. JavaScript lets you throw anything: strings, numbers, plain objects. Code that throws plain objects and code that catches those objects with instanceof Error checks will silently fail to log the error correctly.

Error.isError() is a static method that checks whether a value is a genuine Error object, regardless of realm:

try {
  doSomething();
} catch (e) {
  if (Error.isError(e)) {
    console.error(e.message); // safe
  }
}
Enter fullscreen mode Exit fullscreen mode

The check is specified to work across realm boundaries. An Error from a worker, an Error from an iframe, an Error from a module with its own context: all return true.

This is the kind of feature that does not change how you think about JavaScript, it just makes a thing that was slightly broken work correctly. If you are writing code that deals with cross-realm errors, or if you are writing library code that needs to be defensive about what gets thrown at it, Error.isError() is the right check to use.


Array.fromAsync(): Async Iteration Gets Easier

Array.from() has been in JavaScript since ES2015. It converts iterables and array-like objects into arrays. Array.fromAsync() does the same thing for async iterables.

Before this, if you wanted to collect all values from an async iterator into an array, you wrote this:

const results = [];
for await (const item of asyncIterable) {
  results.push(item);
}
Enter fullscreen mode Exit fullscreen mode

Or you wrote a helper function, or you reached for a library. This is the kind of thing that seems simple but produces slightly different implementations in every codebase because it is just obvious enough to write yourself and just annoying enough that everyone does it slightly differently.

Array.fromAsync() standardizes it:

const results = await Array.fromAsync(asyncIterable);
Enter fullscreen mode Exit fullscreen mode

It also accepts a mapping function, matching Array.from:

const doubled = await Array.fromAsync(asyncNumbers, (n) => n * 2);
Enter fullscreen mode Exit fullscreen mode

And it handles both async iterables and sync iterables that contain Promises:

const resolved = await Array.fromAsync([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3),
]);
// [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

This is useful whenever you are working with async generators, streaming data, or any API that exposes values through an async iterator. Node.js stream APIs, database cursors, paginated API responses: all of these can be cleanly collected with Array.fromAsync.


Import Attributes: Type Safety for Imports

Import attributes let you attach metadata to an import statement. The motivating use case is importing JSON files, CSS modules, and WebAssembly modules in a way that is explicit about the type:

import data from './config.json' with { type: 'json' };
import styles from './component.css' with { type: 'css' };
Enter fullscreen mode Exit fullscreen mode

The with keyword attaches attributes that the runtime uses to validate and process the import. This is a security feature as much as a convenience. Without explicit type declarations, a server could theoretically return a malicious script at a JSON endpoint and have it execute as JavaScript. The attribute makes the expected type explicit, letting the runtime verify it.

TypeScript 5.3+ already supports import attributes syntax. Node.js and major bundlers have been shipping support throughout 2025. With ES2026, this is officially part of the standard.

For TypeScript projects, import attributes work naturally with the type system and let you import JSON configuration files, locale files, and other structured data with full type inference from the file content.


Math.sumPrecise(): When Number Addition Fails You

One of the smaller additions that is easy to miss is Math.sumPrecise(). JavaScript numbers are IEEE 754 floating point, which means floating point addition accumulates errors:

0.1 + 0.2 // 0.30000000000000004
[0.1, 0.2, 0.3, 0.4].reduce((a, b) => a + b, 0) // 1.0000000000000002
Enter fullscreen mode Exit fullscreen mode

This matters when you are summing large arrays of financial or scientific data. Math.sumPrecise() uses a compensated summation algorithm that reduces this error:

Math.sumPrecise([0.1, 0.2, 0.3, 0.4]) // 1.0
Enter fullscreen mode Exit fullscreen mode

For most application code, the standard floating point behavior is fine. For any calculation where precision actually matters, Math.sumPrecise() is a cleaner solution than implementing Kahan summation yourself or reaching for a BigDecimal library.


Using ES2026 Today

Browser support is rolling out throughout 2026. Here is the practical state of each feature:

Temporal: Shipping in Chrome 127+, Firefox 127+, Safari 17.4+. Use @js-temporal/polyfill for full coverage today.

using / await using: TypeScript has supported this syntax since 5.2. Node.js 22+ supports it natively. Bundlers (Vite, webpack, esbuild) transpile it for older environments.

Error.isError: Shipping in all major browsers in 2026. Widely available.

Array.fromAsync: Available in Node.js 22+ and major browsers as of 2026.

Import attributes: Broadly supported in Node.js, Deno, and major bundlers. Browser support is solid across Chromium and Firefox.

For TypeScript users, adding "target": "ES2026" (or "ESNext") to your tsconfig.json and using a modern bundler gets you access to all of these features with transpilation fallbacks for environments that need them.


What This Release Actually Means

ES2026 is not a release that adds new capabilities JavaScript did not have. Temporal replaces things you were doing with date libraries. Explicit resource management replaces patterns you were writing with try/finally. Array.fromAsync replaces a for-await loop you were writing by hand. Error.isError fixes a check you were getting subtly wrong.

That is actually the point. The best language features are not the ones that make new things possible. They are the ones that make common things correct by default.

JavaScript has been carrying decades of quirks. ES2026 addresses a meaningful chunk of them. The date situation alone has generated an entire ecosystem of libraries and untold hours of debugging. That era is ending.

If you have been putting off learning the TypeScript and JavaScript ecosystem changes from the last year, ES2026 is the version that gives you a reason to catch up.


The Short Version

  • Temporal API: Replace every date library with this. Immutable, timezone-aware, no zero-indexed months.
  • using / await using: Stop writing try/finally for resource cleanup. Add [Symbol.dispose] to your resource types.
  • Error.isError(): Use this instead of instanceof Error in catch blocks, especially in library code.
  • Array.fromAsync(): Collect async iterables into arrays in one line.
  • Import attributes: Explicitly type your JSON and CSS imports.
  • Math.sumPrecise(): Precise floating point summation for when it matters.

None of these require rewriting your existing codebase. Start using them in new code, add the polyfills where you need them, and watch the category of bugs each one addresses stop appearing in your projects.

Top comments (0)