refactoredby Sulhadin Öney

JavaScript's New Features in 2025 - How They Actually Work

December 27, 2025

TC39 finished ten proposals in 2025,lets wrapping up the most popular ones. Most articles will tell you what they do. This one tells you how they work.

I’m going to focus on the four that will change how I write JavaScript: Array.fromAsync(), explicit resource management with using, RegExp.escape(), and Error.isError().

Let’s start with the one I’ve wanted for years.

Array.fromAsync() - Finally, a Clean Way to Collect Async Data

Interactive playground:

Array.fromAsync() Interactive Demo

Watch how Array.fromAsync() collects values from an async iterator in real-time

How it works:
  • Creates an async generator that yields numbers with 800ms delays
  • Array.fromAsync() awaits each value as it's yielded
  • Optionally applies a mapping function to each value
  • Returns a complete array once iteration finishes

The Problem

You’re fetching data from an API that returns results in chunks:

async function* fetchUsersPaginated() {
  let page = 1;
  while (true) {
    const response = await fetch(`/api/users?page=${page}`);
    const data = await response.json();
    if (data.users.length === 0) break;
    
    for (const user of data.users) {
      yield user;
    }
    page++;
  }
}

How do you collect all users into an array? Before Array.fromAsync(), you did this:

async function getAllUsers() {
  const users = [];
  for await (const user of fetchUsersPaginated()) {
    users.push(user);
  }
  return users;
}

It works, but it’s verbose. You write this pattern so often it feels like it should be built into the language.

The Solution

const users = await Array.fromAsync(fetchUsersPaginated());

That’s it. One line.

Try It Yourself

Open your browser console and paste this:

// Create a mock async generator
async function* countdown(from) {
  for (let i = from; i > 0; i--) {
    await new Promise(resolve => setTimeout(resolve, 500));
    console.log(`Yielding: ${i}`);
    yield i;
  }
}
 
// Collect all values
console.log('Starting...');
const numbers = await Array.fromAsync(countdown(5));
console.log('Result:', numbers); // [5, 4, 3, 2, 1]

Watch your console - you’ll see each number appear with a 500ms delay, then the final array.

// Try with mapping
const doubled = await Array.fromAsync(
  countdown(3),
  n => n * 2
);
console.log(doubled); // [6, 4, 2]
 
// Try with async mapping
const fetchedData = await Array.fromAsync(
  countdown(3),
  async n => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${n}`);
    return res.json();
  }
);
console.log(fetchedData); // Array of 3 todo objects

How It Works Under the Hood

Here’s a simplified implementation of what Array.fromAsync() does internally:

Array.fromAsync = async function(asyncIterable, mapFn, thisArg) {
  // 1. Create a new empty array
  const result = [];
  
  // 2. Get the async iterator
  const iterator = asyncIterable[Symbol.asyncIterator]();
  
  // 3. Keep calling next() until done
  let index = 0;
  while (true) {
    const { value, done } = await iterator.next();
    
    if (done) break;
    
    // 4. Apply mapping function if provided
    const mappedValue = mapFn 
      ? mapFn.call(thisArg, value, index)
      : value;
    
    // 5. Add to result array
    result[index] = await mappedValue;
    index++;
  }
  
  return result;
};

Key implementation details:

  1. It awaits each iteration - Unlike Array.from() which is synchronous, Array.fromAsync() awaits each next() call. This means it respects backpressure.

  2. It handles both async iterables and promises - You can pass it fetchUsersPaginated() (async iterable) or Promise.resolve([1, 2, 3]) (promise of an array).

  3. The mapping function can be async - Unlike Array.from(), if you provide a mapping function, it can return a promise:

const userIds = await Array.fromAsync(
  fetchUsersPaginated(),
  user => user.id  // Can be async!
);
  1. It creates the array incrementally - It doesn’t wait for all values before starting. It processes them one by one, which is better for memory with large datasets.

When This Breaks (And Why)

const stream = fetchUsersPaginated();
 
// DON'T DO THIS
const users1 = await Array.fromAsync(stream);
const users2 = await Array.fromAsync(stream); // Empty! Iterator is exhausted

Async iterators are consumed. Once you iterate through them, they’re done. This is by design - it matches how streams work.

Real-World Use Case

I use this for collecting WebSocket messages:

async function* listenToWebSocket(ws) {
  const messageQueue = [];
  let resolver;
  
  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (resolver) {
      resolver(data);
      resolver = null;
    } else {
      messageQueue.push(data);
    }
  };
  
  while (ws.readyState === WebSocket.OPEN) {
    if (messageQueue.length > 0) {
      yield messageQueue.shift();
    } else {
      yield new Promise(resolve => { resolver = resolve; });
    }
  }
}
 
// Collect first 10 messages
const messages = await Array.fromAsync(
  listenToWebSocket(ws),
  (msg, index) => index < 10 ? msg : undefined
).then(msgs => msgs.filter(Boolean).slice(0, 10));

Actually, that’s still messy. Better approach:

async function* take(asyncIterable, n) {
  let count = 0;
  for await (const item of asyncIterable) {
    if (count++ >= n) break;
    yield item;
  }
}
 
const first10 = await Array.fromAsync(
  take(listenToWebSocket(ws), 10)
);

This is the power of async iterators - they compose.

Interactive Example: Try It Yourself

Here’s a working demo you can run:

// Simulated async data source (like an API with pagination)
async function* fetchDataInChunks() {
  const data = [
    ['apple', 'banana', 'cherry'],
    ['date', 'elderberry', 'fig'],
    ['grape', 'honeydew', 'kiwi']
  ];
  
  for (const chunk of data) {
    // Simulate network delay
    await new Promise(resolve => setTimeout(resolve, 500));
    console.log(`Fetching chunk: ${chunk.join(', ')}`);
    
    for (const item of chunk) {
      yield item;
    }
  }
}
 
// Using Array.fromAsync to collect everything
async function demo() {
  console.log('Starting to fetch all data...');
  const allItems = await Array.fromAsync(fetchDataInChunks());
  console.log('All items:', allItems);
  // Output: ['apple', 'banana', 'cherry', 'date', 'elderberry', 'fig', 'grape', 'honeydew', 'kiwi']
  
  // With mapping function
  const upperCased = await Array.fromAsync(
    fetchDataInChunks(),
    item => item.toUpperCase()
  );
  console.log('Uppercased:', upperCased);
  // Output: ['APPLE', 'BANANA', 'CHERRY', ...]
}
 
demo();

Try it in your browser console (requires Chrome 123+, Firefox 124+, or polyfill):

// Polyfill for older browsers
if (!Array.fromAsync) {
  Array.fromAsync = async function(asyncIterable, mapFn, thisArg) {
    const result = [];
    let index = 0;
    for await (const value of asyncIterable) {
      const mappedValue = mapFn 
        ? await mapFn.call(thisArg, value, index)
        : value;
      result[index++] = mappedValue;
    }
    return result;
  };
}

Explicit Resource Management (using) - No More Leaked File Handles

Interactive playground:

Using Declarations Interactive Demo

Watch automatic resource cleanup in action (simulating the using keyword)

❌ Without using:
const resource = acquire();
try {
  resource.use();
} finally {
  resource.dispose(); // Manual!
}
✅ With using:
using resource = acquire();
resource.use();
// Automatic cleanup!
Key Features:
  • Resources are disposed automatically when leaving scope
  • Works even if errors are thrown
  • Multiple resources dispose in reverse order
  • Implements Symbol.dispose or Symbol.asyncDispose

The Problem

You open a file, process it, and forget to close it:

async function processFile(path) {
  const file = await fs.open(path);
  
  // Process file...
  const data = await file.read();
  
  // Oops, forgot to close!
  // file.close();
  
  return data;
}

Or you remember to close it, but an error throws before you get there:

async function processFile(path) {
  const file = await fs.open(path);
  
  const data = await file.read();
  
  // If this throws, file never closes
  const parsed = JSON.parse(data);
  
  await file.close();
  return parsed;
}

So you wrap everything in try-finally:

async function processFile(path) {
  const file = await fs.open(path);
  
  try {
    const data = await file.read();
    const parsed = JSON.parse(data);
    return parsed;
  } finally {
    await file.close();
  }
}

This works, but it’s noisy. And you forget to do it. A lot.

The Solution

async function processFile(path) {
  using file = await fs.open(path);
  
  const data = await file.read();
  const parsed = JSON.parse(data);
  return parsed;
  
  // file.close() is called automatically, even if an error throws
}

The using keyword automatically calls cleanup when the variable goes out of scope.

Try It Yourself (Polyfill Version)

Since using requires transpilation, here’s a working example you can run today:

// Create a disposable resource
class Timer {
  constructor(name) {
    this.name = name;
    this.start = Date.now();
    console.log(`⏱️  ${name} started`);
  }
  
  log(message) {
    console.log(`[${this.name}] ${message}`);
  }
  
  [Symbol.dispose]() {
    const elapsed = Date.now() - this.start;
    console.log(`⏱️  ${this.name} finished (${elapsed}ms)`);
  }
}
 
// Manual disposal (what we do now)
function withoutUsing() {
  const timer = new Timer('Manual');
  try {
    timer.log('Doing work...');
    // Do work...
  } finally {
    timer[Symbol.dispose]();
  }
}
 
// With using (simulated with polyfill)
function withUsing() {
  // This simulates what `using timer = new Timer('Auto')` does
  const timer = new Timer('Auto');
  const dispose = timer[Symbol.dispose].bind(timer);
  
  try {
    timer.log('Doing work...');
    // Do work...
    // Automatic disposal happens even if error throws
    throw new Error('Oops!');
  } catch (e) {
    console.log('Caught:', e.message);
  } finally {
    dispose();
  }
}
 
// Try both
withoutUsing();
withUsing();
class DatabaseConnection {
  constructor(id) {
    this.id = id;
    this.queries = 0;
    console.log(`🔌 Connection ${id} opened`);
  }
  
  async query(sql) {
    this.queries++;
    await new Promise(r => setTimeout(r, 100));
    console.log(`  Query ${this.queries}: ${sql.substring(0, 30)}...`);
    return { rows: [] };
  }
  
  async [Symbol.asyncDispose]() {
    console.log(`🔌 Connection ${this.id} closed (${this.queries} queries)`);
    await new Promise(r => setTimeout(r, 50));
  }
}
 
// Try this in console
async function testConnection() {
  const conn = new DatabaseConnection(1);
  
  try {
    await conn.query('SELECT * FROM users');
    await conn.query('SELECT * FROM posts');
    // Simulate error
    throw new Error('Query failed!');
  } catch (e) {
    console.log('❌ Error:', e.message);
  } finally {
    await conn[Symbol.asyncDispose]();
  }
}
 
testConnection();

How It Works Under the Hood

The using declaration is syntactic sugar for try-finally with a specific cleanup protocol.

What the engine does:

// You write:
using file = await fs.open(path);
doSomething(file);
 
// Engine transforms to roughly:
{
  const file = await fs.open(path);
  const disposeMethod = file[Symbol.dispose] || file[Symbol.asyncDispose];
  
  try {
    doSomething(file);
  } finally {
    if (Symbol.asyncDispose in file) {
      await disposeMethod.call(file);
    } else {
      disposeMethod.call(file);
    }
  }
}

The protocol:

Objects opt into this by implementing Symbol.dispose or Symbol.asyncDispose:

class FileHandle {
  constructor(fd) {
    this.fd = fd;
    this.closed = false;
  }
  
  async read() {
    if (this.closed) throw new Error('File is closed');
    // Read implementation...
  }
  
  // This method is called automatically by `using`
  async [Symbol.asyncDispose]() {
    if (!this.closed) {
      await this.close();
      this.closed = true;
    }
  }
  
  async close() {
    // Actual close implementation
    return fs.promises.close(this.fd);
  }
}

Multiple resources:

async function processFiles(path1, path2) {
  using file1 = await fs.open(path1);
  using file2 = await fs.open(path2);
  
  // Both close automatically in reverse order:
  // 1. file2 closes
  // 2. file1 closes
}

This is important! Resources dispose in reverse order of declaration. Just like destructors in C++.

Why reverse order? Because later resources might depend on earlier ones:

using connection = await db.connect();
using transaction = await connection.beginTransaction();
 
// transaction must commit/rollback before connection closes

The Tricky Part: Suppressed Errors

What happens if disposal throws?

class BrokenResource {
  async [Symbol.asyncDispose]() {
    throw new Error('Cleanup failed!');
  }
}
 
async function test() {
  using resource = new BrokenResource();
  throw new Error('Main error');
}

You have two errors: the main error and the disposal error. Which one do you get?

Answer: The main error. The disposal error is “suppressed.”

Under the hood:

{
  const resource = new BrokenResource();
  let mainError;
  
  try {
    throw new Error('Main error');
  } catch (e) {
    mainError = e;
  } finally {
    try {
      await resource[Symbol.asyncDispose]();
    } catch (disposeError) {
      // Suppressed! Only logged, not thrown
      if (mainError) {
        // Attach to main error as a property
        mainError.suppressed = [disposeError];
      } else {
        // No main error, so disposal error becomes the error
        throw disposeError;
      }
    }
  }
  
  if (mainError) throw mainError;
}

This matches how finally blocks work in other languages (C#, Java, Python).

Real-World Use Case

Database transactions:

class Transaction {
  constructor(db) {
    this.db = db;
    this.active = true;
  }
  
  async query(sql) {
    if (!this.active) throw new Error('Transaction inactive');
    return this.db.query(sql);
  }
  
  async commit() {
    await this.db.query('COMMIT');
    this.active = false;
  }
  
  async rollback() {
    await this.db.query('ROLLBACK');
    this.active = false;
  }
  
  async [Symbol.asyncDispose]() {
    if (this.active) {
      await this.rollback();
    }
  }
}
 
async function transferMoney(from, to, amount) {
  using tx = new Transaction(db);
  
  await tx.query('UPDATE accounts SET balance = balance - ? WHERE id = ?', 
    [amount, from]);
  await tx.query('UPDATE accounts SET balance = balance + ? WHERE id = ?', 
    [amount, to]);
  
  await tx.commit();
  
  // If any error threw before commit, rollback happens automatically
}

Interactive Example: See Automatic Cleanup in Action

// Visual demo of using declarations
class Resource {
  constructor(name) {
    this.name = name;
    this.timestamp = Date.now();
    console.log(`✅ ${name} acquired at ${new Date().toLocaleTimeString()}`);
  }
  
  use() {
    console.log(`🔧 Using ${this.name}`);
  }
  
  [Symbol.dispose]() {
    const duration = Date.now() - this.timestamp;
    console.log(`❌ ${this.name} disposed after ${duration}ms`);
  }
}
 
// Simulating 'using' with try-finally (since using needs transpilation)
function demoManualCleanup() {
  console.log('--- Manual Cleanup Demo ---');
  
  const r1 = new Resource('Database Connection');
  const r2 = new Resource('File Handle');
  const r3 = new Resource('Network Socket');
  
  try {
    r1.use();
    r2.use();
    r3.use();
    
    // Simulate error
    throw new Error('Something went wrong!');
  } catch (e) {
    console.log(`⚠️  Caught error: ${e.message}`);
  } finally {
    // Must manually dispose in reverse order
    r3[Symbol.dispose]();
    r2[Symbol.dispose]();
    r1[Symbol.dispose]();
  }
}
 
// Run it
demoManualCleanup();
 
// Demo showing multiple resources with automatic cleanup order
function demoMultipleResources() {
  console.log('\n--- Multiple Resources Demo ---');
  
  const resources = [];
  
  function using_(name) {
    const resource = new Resource(name);
    resources.push(resource);
    return resource;
  }
  
  try {
    const db = using_('Database');
    const cache = using_('Cache (depends on DB)');
    const session = using_('Session (depends on Cache)');
    
    db.use();
    cache.use();
    session.use();
    
    console.log('All resources in use...');
  } finally {
    // Dispose in REVERSE order (important!)
    while (resources.length > 0) {
      resources.pop()[Symbol.dispose]();
    }
  }
}
 
demoMultipleResources();

Expected output:

--- Manual Cleanup Demo ---
✅ Database Connection acquired at 2:30:45 PM
✅ File Handle acquired at 2:30:45 PM
✅ Network Socket acquired at 2:30:45 PM
🔧 Using Database Connection
🔧 Using File Handle
🔧 Using Network Socket
⚠️  Caught error: Something went wrong!
❌ Network Socket disposed after 5ms
❌ File Handle disposed after 6ms
❌ Database Connection disposed after 7ms
 
--- Multiple Resources Demo ---
✅ Database acquired at 2:30:45 PM
✅ Cache (depends on DB) acquired at 2:30:45 PM
✅ Session (depends on Cache) acquired at 2:30:45 PM
🔧 Using Database
🔧 Using Cache (depends on DB)
🔧 Using Session (depends on Cache)
All resources in use...
❌ Session (depends on Cache) disposed after 3ms
❌ Cache (depends on DB) disposed after 4ms
❌ Database disposed after 5ms

Notice how resources are disposed in reverse order - Session → Cache → Database. This ensures dependent resources are cleaned up first.

RegExp.escape() - Stop Manually Escaping Regex Characters

Interactive playground:

RegExp.escape() Interactive Demo

See how special regex characters break searches without escaping

Quick Presets:
Characters that need escaping:
^ $ \ . * + ? ( ) [ ] {} |
Without escaping, these are interpreted as regex special characters and will cause errors or unexpected matches.

The Problem

You want to search for user input in a string:

function findInText(text, search) {
  const regex = new RegExp(search, 'gi');
  return text.match(regex);
}
 
findInText('Price: $50', '$50');  // Doesn't work! $ is a regex special char

You need to escape special characters:

function escapeRegex(string) {
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
 
function findInText(text, search) {
  const escaped = escapeRegex(search);
  const regex = new RegExp(escaped, 'gi');
  return text.match(regex);
}

Every project has this function. Now it’s built in.

The Solution

function findInText(text, search) {
  const regex = new RegExp(RegExp.escape(search), 'gi');
  return text.match(regex);
}

How It Works Under the Hood

RegExp.escape() escapes these characters:

^ $ \ . * + ? ( ) [ ] { } |

Simple implementation:

RegExp.escape = function(string) {
  return string.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
};

But the actual spec is more nuanced. It handles:

  1. Line terminators - \n, \r, \u2028, \u2029 are escaped
  2. Control characters - Some control chars get special treatment
  3. Unicode - Handles surrogate pairs correctly

More accurate implementation:

RegExp.escape = function(S) {
  let result = '';
  for (let i = 0; i < S.length; i++) {
    const c = S[i];
    const code = S.charCodeAt(i);
    
    // Syntax characters that need escaping
    if ('^$\\.*+?()[]{}|'.includes(c)) {
      result += '\\' + c;
      continue;
    }
    
    // Line terminators
    if (code === 0x000A) { result += '\\n'; continue; }  // \n
    if (code === 0x000D) { result += '\\r'; continue; }  // \r
    if (code === 0x2028) { result += '\\u2028'; continue; }
    if (code === 0x2029) { result += '\\u2029'; continue; }
    
    // Forward slash (optional, but helps with regex literals)
    if (c === '/') {
      result += '\\/';
      continue;
    }
    
    // Everything else passes through
    result += c;
  }
  
  return result;
};

Why escape line terminators?

const userInput = 'First line\nSecond line';
const pattern = new RegExp(RegExp.escape(userInput));
 
// Without escaping \n, this would create:
// /First line
// Second line/
// which is a syntax error!
 
// With escaping:
// /First line\nSecond line/
// which works correctly

Real-World Use Case

Highlighting search results:

function highlightSearch(text, searchTerm) {
  const escaped = RegExp.escape(searchTerm);
  const regex = new RegExp(`(${escaped})`, 'gi');
  
  return text.replace(regex, '<mark>$1</mark>');
}
 
highlightSearch(
  'The price is $49.99',
  '$49.99'
);
// "The price is <mark>$49.99</mark>"

Without RegExp.escape(), $49.99 would be interpreted as regex syntax and fail.

Interactive Example: Test RegExp.escape()

// Polyfill (works in all browsers)
if (!RegExp.escape) {
  RegExp.escape = function(string) {
    return string.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
      .replace(/\n/g, '\\n')
      .replace(/\r/g, '\\r')
      .replace(/\//g, '\\/');
  };
}
 
// Test cases that commonly break without escaping
const testCases = [
  { text: 'Price: $49.99', search: '$49.99' },
  { text: 'Email: user@example.com', search: 'user@example.com' },
  { text: 'Formula: (a + b) * c', search: '(a + b)' },
  { text: 'Path: C:\\Users\\Documents', search: 'C:\\Users' },
  { text: 'Regex: /test.*pattern/', search: '/test.*pattern/' },
  { text: 'Array: [1, 2, 3]', search: '[1, 2, 3]' }
];
 
function testWithoutEscape(text, search) {
  try {
    const regex = new RegExp(search, 'g');
    const matches = text.match(regex);
    return matches ? '✅ Found: ' + matches.join(', ') : '❌ No match';
  } catch (e) {
    return '💥 ERROR: ' + e.message;
  }
}
 
function testWithEscape(text, search) {
  try {
    const escaped = RegExp.escape(search);
    const regex = new RegExp(escaped, 'g');
    const matches = text.match(regex);
    return matches ? '✅ Found: ' + matches.join(', ') : '❌ No match';
  } catch (e) {
    return '💥 ERROR: ' + e.message;
  }
}
 
// Run all tests
console.log('RegExp.escape() Demo\n' + '='.repeat(50));
 
testCases.forEach(({ text, search }) => {
  console.log(`\nSearching for: "${search}" in "${text}"`);
  console.log('  Without escape:', testWithoutEscape(text, search));
  console.log('  With escape:   ', testWithEscape(text, search));
  console.log('  Escaped form:  ', RegExp.escape(search));
});

Try it yourself - paste this in console:

// Interactive highlighter
function highlightSearch(text, searchTerm) {
  const escaped = RegExp.escape(searchTerm);
  const regex = new RegExp(`(${escaped})`, 'gi');
  return text.replace(regex, '👉 $1 👈');
}
 
// Test it
console.log(highlightSearch('The price is $49.99', '$49.99'));
// Output: "The price is 👉 $49.99 👈"
 
console.log(highlightSearch('Email: test@example.com', 'test@example.com'));
// Output: "Email: 👉 test@example.com 👈"
 
console.log(highlightSearch('Use (parentheses) here', '(parentheses)'));
// Output: "Use 👉 (parentheses) 👈 here"

Expected full output:

RegExp.escape() Demo
==================================================
 
Searching for: "$49.99" in "Price: $49.99"
  Without escape: 💥 ERROR: Invalid regular expression
  With escape:    ✅ Found: $49.99
  Escaped form:   \$49\.99
 
Searching for: "user@example.com" in "Email: user@example.com"
  Without escape: ❌ No match
  With escape:    ✅ Found: user@example.com
  Escaped form:   user@example\.com
 
Searching for: "(a + b)" in "Formula: (a + b) * c"
  Without escape: 💥 ERROR: Invalid regular expression
  With escape:    ✅ Found: (a + b)
  Escaped form:   \(a \+ b\)
 
Searching for: "C:\Users" in "Path: C:\Users\Documents"
  Without escape: 💥 ERROR: Invalid regular expression
  With escape:    ✅ Found: C:\Users
  Escaped form:   C:\\Users
 
Searching for: "/test.*pattern/" in "Regex: /test.*pattern/"
  Without escape: ❌ No match (. and * are special chars)
  With escape:    ✅ Found: /test.*pattern/
  Escaped form:   \/test\.\*pattern\/
 
Searching for: "[1, 2, 3]" in "Array: [1, 2, 3]"
  Without escape: 💥 ERROR: Invalid regular expression
  With escape:    ✅ Found: [1, 2, 3]
  Escaped form:   \[1, 2, 3\]

Error.isError() - Fixing the Cross-Realm Problem

Interactive playground:

Error.isError() Interactive Demo

Test the difference between Error.isError() and instanceof Error

Why Error.isError() matters:
  • instanceof Error fails with errors from iframes, workers, or Node.js vm modules
  • Error.isError() checks internal slots instead of prototype chain
  • Works reliably across all JavaScript realms
  • Correctly rejects objects that just look like errors

The Problem

You check if something is an error:

function handleError(err) {
  if (err instanceof Error) {
    console.log(err.message);
  }
}

This breaks in subtle ways:

// In an iframe
const iframeError = iframe.contentWindow.eval('new Error("Oops")');
 
handleError(iframeError);
// Doesn't work! instanceof checks against the wrong Error constructor

Each JavaScript realm (iframe, worker, vm module in Node) has its own Error constructor. instanceof checks against the current realm’s constructor.

The Solution

function handleError(err) {
  if (Error.isError(err)) {
    console.log(err.message);
  }
}

Error.isError() works across realms.

How It Works Under the Hood

The spec defines errors by their internal slots, not their prototype chain.

Conceptual implementation:

Error.isError = function(value) {
  // Check if the object has [[ErrorData]] internal slot
  // This is a spec-level check that engines implement natively
  return HasInternalSlot(value, '[[ErrorData]]');
};

You can’t actually implement this in JavaScript because internal slots aren’t accessible. But here’s what the engine checks:

  1. Is value an object?
  2. Does it have the [[ErrorData]] internal slot?

All these have [[ErrorData]]:

  • new Error()
  • new TypeError()
  • new RangeError()
  • Errors from other realms
  • Object.create(Error.prototype) (has the slot!)

These don’t:

  • { message: 'error' }
  • Object.create(null)
  • Plain objects that look like errors

Interesting edge case:

const notReallyAnError = Object.create(Error.prototype);
notReallyAnError.message = 'Looks like an error';
 
Error.isError(notReallyAnError);  // true!
notReallyAnError instanceof Error;  // true
 
// Both work because Object.create(Error.prototype) gets [[ErrorData]]

Why not just use duck typing?

// Why not just check for .message and .stack?
function isError(value) {
  return value && 
         typeof value.message === 'string' && 
         typeof value.stack === 'string';
}

Because objects can fake it:

const fake = { message: 'error', stack: 'fake' };
isError(fake);  // true
Error.isError(fake);  // false - correctly identifies it's not real

Real-World Use Case

Logging with Sentry/error tracking:

function logError(error, context) {
  if (Error.isError(error)) {
    // Real error - send full stack trace
    Sentry.captureException(error, {
      contexts: { custom: context }
    });
  } else {
    // Not a real error - log as message
    Sentry.captureMessage(JSON.stringify(error), {
      level: 'error',
      contexts: { custom: context }
    });
  }
}
 
// Works correctly even with errors from iframes, workers, etc.
window.addEventListener('error', (event) => {
  logError(event.error, { source: 'window.onerror' });
});

Interactive Example: Test Error.isError()

// Polyfill for Error.isError
if (!Error.isError) {
  Error.isError = function(value) {
    // This is a simplified polyfill - the real implementation
    // checks internal slots, which we can't do in pure JS
    // This approximation works for most cases
    return value instanceof Error ||
           (value && value.constructor && value.constructor.name === 'Error') ||
           Object.prototype.toString.call(value) === '[object Error]';
  };
}
 
// Test different error types
const testValues = [
  { name: 'Regular Error', value: new Error('test') },
  { name: 'TypeError', value: new TypeError('test') },
  { name: 'Custom Error subclass', value: new (class CustomError extends Error {})('test') },
  { name: 'Object with Error prototype', value: Object.create(Error.prototype) },
  { name: 'Plain object', value: { message: 'error', stack: 'fake stack' } },
  { name: 'Error-like object', value: { name: 'Error', message: 'test' } },
  { name: 'String', value: 'Error: something went wrong' },
  { name: 'Null', value: null },
  { name: 'Undefined', value: undefined },
];
 
console.log('Error.isError() Test Results\n' + '='.repeat(60));
 
testValues.forEach(({ name, value }) => {
  const isErrorResult = Error.isError(value);
  const instanceOfResult = value instanceof Error;
  const icon = isErrorResult ? '' : '';
  
  console.log(`\n${name}:`);
  console.log(`  Error.isError(): ${icon} ${isErrorResult}`);
  console.log(`  instanceof Error: ${instanceOfResult ? '' : ''} ${instanceOfResult}`);
  
  if (isErrorResult !== instanceOfResult) {
    console.log(`  ⚠️  Different results!`);
  }
});
 
// Demonstrate cross-realm behavior
console.log('\n' + '='.repeat(60));
console.log('Cross-Realm Test (iframe simulation)\n');
 
// Create an iframe (if in browser)
if (typeof document !== 'undefined') {
  const iframe = document.createElement('iframe');
  iframe.style.display = 'none';
  document.body.appendChild(iframe);
  
  // Create error in iframe realm
  const iframeError = new iframe.contentWindow.Error('From iframe');
  
  console.log('Error created in iframe:');
  console.log('  Error.isError():', Error.isError(iframeError) ? '✅ true' : '❌ false');
  console.log('  instanceof Error:', iframeError instanceof Error ? '✅ true' : '❌ false');
  console.log('  instanceof iframe.contentWindow.Error:', 
    iframeError instanceof iframe.contentWindow.Error ? '✅ true' : '❌ false');
  
  // Clean up
  document.body.removeChild(iframe);
} else {
  console.log('(Browser-only demo - run in browser to see cross-realm behavior)');
}
 
// Practical use case: Smart error handler
function smartErrorHandler(error) {
  if (Error.isError(error)) {
    // Real error - full logging
    console.log('📋 Error Details:');
    console.log('  Name:', error.name);
    console.log('  Message:', error.message);
    console.log('  Stack:', error.stack?.split('\n')[0]);
  } else {
    // Not a real error - log as string
    console.log('⚠️  Non-error value:', typeof error, error);
  }
}
 
// Test the handler
console.log('\n' + '='.repeat(60));
console.log('Smart Error Handler Demo\n');
 
smartErrorHandler(new Error('Real error'));
smartErrorHandler({ message: 'Fake error' });
smartErrorHandler('String error');

Expected output:

Error.isError() Test Results
============================================================
 
Regular Error:
  Error.isError(): ✅ true
  instanceof Error: ✅ true
 
TypeError:
  Error.isError(): ✅ true
  instanceof Error: ✅ true
 
Custom Error subclass:
  Error.isError(): ✅ true
  instanceof Error: ✅ true
 
Object with Error prototype:
  Error.isError(): ✅ true
  instanceof Error: ✅ true
 
Plain object:
  Error.isError(): ❌ false
  instanceof Error: ❌ false
 
Error-like object:
  Error.isError(): ❌ false
  instanceof Error: ❌ false
 
String:
  Error.isError(): ❌ false
  instanceof Error: ❌ false
 
Null:
  Error.isError(): ❌ false
  instanceof Error: ❌ false
 
Undefined:
  Error.isError(): ❌ false
  instanceof Error: ❌ false
 
============================================================
Cross-Realm Test (iframe simulation)
 
Error created in iframe:
  Error.isError(): ✅ true
  instanceof Error: ❌ false    ← Different!
  instanceof iframe.contentWindow.Error: ✅ true
 
============================================================
Smart Error Handler Demo
 
📋 Error Details:
  Name: Error
  Message: Real error
  Stack: Error: Real error
 
⚠️  Non-error value: object { message: 'Fake error' }
 
⚠️  Non-error value: string String error

Notice how Error.isError() returns true for the iframe error, but instanceof Error returns false. This is the cross-realm problem that Error.isError() solves!

Other Notable Proposals

Float16Array

Interactive playground:

Float16Array Interactive Demo

Compare precision and memory usage of different float types

Memory Usage (for 1,000 numbers)
Float16Array
1.95 KB
-75.0%
Float32Array
3.91 KB
-50.0%
Float64Array
7.81 KB
baseline
Use Cases:
  • ML/AI: Model weights where high precision isn't critical (50% memory savings)
  • GPU: Texture data and graphics where 16-bit is sufficient
  • Audio: Signal processing where storage matters more than precision
  • IoT: Sensor data transmission where bandwidth is limited

16-bit floating point typed arrays for GPU/ML work:

const float16 = new Float16Array([1.5, 2.5, 3.5]);
 
// More memory efficient than Float32Array
// Useful for ML model weights, GPU textures
 
const rounded = Math.f16round(1.23456);  // 1.234

How it works: Uses IEEE 754 half-precision format (1 sign bit, 5 exponent bits, 10 fraction bits). Internally stored as 16-bit integers, converted to 64-bit floats for computation.

Interactive Example: Float16 vs Float32 vs Float64

// Create arrays with the same values
const values = [1.123456789, 2.987654321, 3.141592653];
 
const float16 = new Float16Array(values);
const float32 = new Float32Array(values);
const float64 = new Float64Array(values);
 
console.log('Float16Array Demo\n' + '='.repeat(60));
 
// Compare precision
console.log('\nPrecision Comparison:');
values.forEach((original, i) => {
  console.log(`\nOriginal value: ${original}`);
  console.log(`  Float16: ${float16[i]} (${((float16[i] - original) * 100).toFixed(4)}% error)`);
  console.log(`  Float32: ${float32[i]} (${((float32[i] - original) * 100).toFixed(4)}% error)`);
  console.log(`  Float64: ${float64[i]} (${((float64[i] - original) * 100).toFixed(4)}% error)`);
});
 
// Compare memory usage
console.log('\n' + '='.repeat(60));
console.log('Memory Usage (for 1000 numbers):');
console.log(`  Float16Array: ${new Float16Array(1000).byteLength} bytes`);
console.log(`  Float32Array: ${new Float32Array(1000).byteLength} bytes`);
console.log(`  Float64Array: ${new Float64Array(1000).byteLength} bytes`);
 
const savings16vs32 = ((1 - (new Float16Array(1000).byteLength / new Float32Array(1000).byteLength)) * 100);
const savings16vs64 = ((1 - (new Float16Array(1000).byteLength / new Float64Array(1000).byteLength)) * 100);
 
console.log(`\nMemory Savings:`);
console.log(`  Float16 vs Float32: ${savings16vs32}% smaller`);
console.log(`  Float16 vs Float64: ${savings16vs64}% smaller`);
 
// Test Math.f16round
console.log('\n' + '='.repeat(60));
console.log('Math.f16round() Demo:');
 
const testNumbers = [1.23456789, 999.999, 0.00001, Math.PI];
testNumbers.forEach(num => {
  const rounded = Math.f16round(num);
  console.log(`  ${num}${rounded}`);
});
 
// Visualize precision loss
console.log('\n' + '='.repeat(60));
console.log('Visual Precision Comparison:\n');
 
function visualizeFloat(value, bits) {
  const precision = bits === 16 ? 3 : bits === 32 ? 7 : 15;
  const stars = ''.repeat(precision);
  const empty = ''.repeat(15 - precision);
  return `${stars}${empty}`;
}
 
console.log('Precision visualization:');
console.log(`  Float16 (10 bit mantissa):  ${visualizeFloat(0, 16)}`);
console.log(`  Float32 (23 bit mantissa):  ${visualizeFloat(0, 32)}`);
console.log(`  Float64 (52 bit mantissa):  ${visualizeFloat(0, 64)}`);
 
// Use case: ML model weights
console.log('\n' + '='.repeat(60));
console.log('Use Case: ML Model Weights (1 million parameters)\n');
 
const modelSize16 = (1_000_000 * 16) / 8 / 1024 / 1024;
const modelSize32 = (1_000_000 * 32) / 8 / 1024 / 1024;
 
console.log(`Model size with Float32: ${modelSize32.toFixed(2)} MB`);
console.log(`Model size with Float16: ${modelSize16.toFixed(2)} MB`);
console.log(`Savings: ${modelSize32 - modelSize16.toFixed(2)} MB (${((1 - modelSize16/modelSize32) * 100).toFixed(1)}% smaller)`);
console.log(`\nDownload time at 10 Mbps:`);
console.log(`  Float32: ${(modelSize32 * 8 / 10).toFixed(1)} seconds`);
console.log(`  Float16: ${(modelSize16 * 8 / 10).toFixed(1)} seconds`);

Expected output:

Precision Comparison:
 
Original value: 1.123456789
  Float16: 1.123046875 (-0.0365% error)
  Float32: 1.1234568357467651 (0.0000% error)
  Float64: 1.123456789 (0.0000% error)
 
Original value: 2.987654321
  Float16: 2.98828125 (0.0210% error)
  Float32: 2.9876542091369629 (-0.0000% error)
  Float64: 2.987654321 (0.0000% error)
 
Original value: 3.141592653
  Float16: 3.140625 (-0.0308% error)
  Float32: 3.1415927410125732 (0.0000% error)
  Float64: 3.141592653 (0.0000% error)
 
============================================================
Memory Usage (for 1000 numbers):
  Float16Array: 2000 bytes
  Float32Array: 4000 bytes
  Float64Array: 8000 bytes
 
Memory Savings:
  Float16 vs Float32: 50% smaller
  Float16 vs Float64: 75% smaller
 
============================================================
Math.f16round() Demo:
  1.23456789 → 1.234375
  999.999 → 1000
  0.00001 → 0.0000099945068359375
  3.141592653589793 → 3.140625
 
============================================================
Visual Precision Comparison:
 
Precision visualization:
  Float16 (10 bit mantissa):  ★★★☆☆☆☆☆☆☆☆☆☆☆☆
  Float32 (23 bit mantissa):  ★★★★★★★☆☆☆☆☆☆☆☆
  Float64 (52 bit mantissa):  ★★★★★★★★★★★★★★★
 
============================================================
Use Case: ML Model Weights (1 million parameters)
 
Model size with Float32: 3.81 MB
Model size with Float16: 1.91 MB
Savings: 1.91 MB (50.0% smaller)
 
Download time at 10 Mbps:
  Float32: 3.0 seconds
  Float16: 1.5 seconds

Math.sumPrecise()

Avoids floating-point accumulation errors:

// Normal sum loses precision
[0.1, 0.2, 0.3].reduce((a, b) => a + b, 0);  // 0.6000000000000001
 
// Precise sum
Math.sumPrecise([0.1, 0.2, 0.3]);  // 0.6

How it works: Uses Kahan summation algorithm internally - maintains a separate compensation variable to track lost precision.

Putting It All Together: Real-World Example

Here’s how these features work together in a realistic application:

// Simulated database connection with automatic cleanup
class DatabaseConnection {
  constructor() {
    this.connected = true;
    console.log('🔌 Database connected');
  }
  
  async query(sql) {
    if (!this.connected) throw new Error('Database disconnected');
    await new Promise(r => setTimeout(r, 100));
    return { rows: [] };
  }
  
  async [Symbol.asyncDispose]() {
    this.connected = false;
    console.log('🔌 Database disconnected');
  }
}
 
// Async iterator for paginated results
async function* fetchUsersWithSearch(db, searchTerm) {
  let page = 1;
  
  while (true) {
    // Use RegExp.escape for safe SQL LIKE queries
    const escapedTerm = RegExp.escape(searchTerm);
    const sql = `SELECT * FROM users WHERE name LIKE '%${escapedTerm}%' LIMIT 10 OFFSET ${(page - 1) * 10}`;
    
    const result = await db.query(sql);
    
    if (result.rows.length === 0) break;
    
    for (const user of result.rows) {
      yield user;
    }
    
    page++;
  }
}
 
// Main application using all new features
async function processUsers(searchTerm) {
  // using: automatic cleanup
  using db = new DatabaseConnection();
  
  try {
    // Array.fromAsync: collect async iterator
    const users = await Array.fromAsync(
      fetchUsersWithSearch(db, searchTerm),
      user => ({
        ...user,
        processedAt: new Date()
      })
    );
    
    console.log(`✅ Found ${users.length} users`);
    return users;
    
  } catch (error) {
    // Error.isError: proper error detection
    if (Error.isError(error)) {
      console.error('❌ Database error:', error.message);
      throw error;
    } else {
      console.error('⚠️  Unknown error:', error);
      throw new Error('Unknown error occurred');
    }
  }
  
  // Database connection automatically closes here (using cleanup)
}
 
// Run the demo
processUsers('john@example.com').then(users => {
  console.log('Processing complete');
}).catch(err => {
  console.error('Failed:', err);
});

Interactive Comparison: Before vs After

console.log('='.repeat(70));
console.log('Before TC39 2025 Features vs After');
console.log('='.repeat(70));
 
// BEFORE: Manual everything
async function processUsersBefore(searchTerm) {
  const db = new DatabaseConnection();
  const users = [];
  
  try {
    let page = 1;
    
    while (true) {
      // Manual regex escaping (easy to forget)
      const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      const sql = `SELECT * FROM users WHERE name LIKE '%${escapedTerm}%' LIMIT 10 OFFSET ${(page - 1) * 10}`;
      
      const result = await db.query(sql);
      
      if (result.rows.length === 0) break;
      
      // Manual async iteration
      for (const user of result.rows) {
        users.push({
          ...user,
          processedAt: new Date()
        });
      }
      
      page++;
    }
    
    return users;
    
  } catch (error) {
    // Unreliable error detection
    if (error instanceof Error) {
      console.error('Database error:', error.message);
      throw error;
    } else {
      console.error('Unknown error:', error);
      throw new Error('Unknown error occurred');
    }
  } finally {
    // Manual cleanup (easy to forget)
    await db[Symbol.asyncDispose]();
  }
}
 
// AFTER: Clean and automatic
async function processUsersAfter(searchTerm) {
  using db = new DatabaseConnection();  // ← Automatic cleanup
  
  try {
    const users = await Array.fromAsync(  // ← One line instead of 10
      fetchUsersWithSearch(db, searchTerm),  // ← Uses RegExp.escape internally
      user => ({
        ...user,
        processedAt: new Date()
      })
    );
    
    return users;
    
  } catch (error) {
    if (Error.isError(error)) {  // ← Works across realms
      console.error('Database error:', error.message);
      throw error;
    } else {
      console.error('Unknown error:', error);
      throw new Error('Unknown error occurred');
    }
  }
}
 
// Line count comparison
const beforeLines = processUsersBefore.toString().split('\n').length;
const afterLines = processUsersAfter.toString().split('\n').length;
 
console.log('\nCode Comparison:');
console.log(`  Before: ${beforeLines} lines`);
console.log(`  After:  ${afterLines} lines`);
console.log(`  Reduction: ${beforeLines - afterLines} lines (${Math.round((1 - afterLines/beforeLines) * 100)}% shorter)`);
console.log('\nBenefits:');
console.log('  ✅ Automatic resource cleanup');
console.log('  ✅ No manual async iteration');
console.log('  ✅ Built-in regex escaping');
console.log('  ✅ Cross-realm error detection');
console.log('  ✅ Less boilerplate');
console.log('  ✅ Fewer bugs');

Expected output:

======================================================================
Before TC39 2025 Features vs After
======================================================================
 
Code Comparison:
  Before: 31 lines
  After:  19 lines
  Reduction: 12 lines (39% shorter)
 
Benefits:
  ✅ Automatic resource cleanup
  ✅ No manual async iteration
  ✅ Built-in regex escaping
  ✅ Cross-realm error detection
  ✅ Less boilerplate
  ✅ Fewer bugs
 
🔌 Database connected
✅ Found 5 users
🔌 Database disconnected
Processing complete

Browser Support

As of January 2025:

  • Chrome 123+: All features
  • Firefox 124+: All features
  • Safari 17.4+: All features
  • Node.js 22+: All features

Polyfills:

npm install core-js@3.36
import 'core-js/actual/array/from-async';
import 'core-js/actual/regexp/escape';
import 'core-js/actual/error/is-error';

The using declaration requires transpilation (Babel 7.24+):

npm install --save-dev @babel/plugin-proposal-explicit-resource-management

When Should You Use These?

Array.fromAsync() - Use today with a polyfill. It’s backward compatible and just makes async iteration cleaner.

using declarations - Use in TypeScript/Babel projects. Requires transpilation but the ergonomics are worth it for resource-heavy code.

RegExp.escape() - Use today with a polyfill. No downsides, only benefits.

Error.isError() - Use when dealing with cross-realm code (iframes, workers). Otherwise instanceof Error is fine.

The Bigger Picture

These aren’t flashy features. They don’t change how you think about JavaScript. They’re small improvements that remove papercuts.

But that’s exactly what makes them good. They solve real problems without introducing new complexity.

Array.fromAsync() eliminates a pattern we’ve written hundreds of times.
using prevents entire classes of resource leak bugs.
RegExp.escape() fixes a function every codebase reinvents.
Error.isError() makes error handling work correctly across realms.

These are the kind of features that compound. Each one saves you 5 minutes, 10 lines of code, one bug. Over thousands of codebases and millions of developers, that adds up.

This is TC39 working as intended.