Back to blog

Node.js 24 LTS: All New Features from V8 13.6, npm 11 and Performance Improvements

Hello HaWkers, Node.js 24 officially arrived in May 2025 and is scheduled to enter LTS (Long-Term Support) in October. This version brings an impressive amount of new JavaScript features, performance improvements, and modernized APIs.

If you work with JavaScript on the backend, this guide will show you everything you need to know about the new version.

Node.js 24 Overview

Node.js 24 represents a significant leap in capabilities.

Main updates:

Component Previous Version Node.js 24
V8 Engine 12.x 13.6
npm 10.x 11
Undici 6.x 7
libuv 1.48 1.50

Support timeline:

  • Launch: May 2025
  • Active LTS: October 2025 - April 2027
  • Maintenance: April 2027 - April 2028

New JavaScript Features

V8 13.6 unlocks several modern ECMAScript features.

RegExp.escape

We finally have a native way to escape strings for use in regular expressions.

// Before - manual function (prone to bugs)
function escapeRegExp(string) {
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// Now - native method
const userInput = "Hello [World]! Price: $100";
const escaped = RegExp.escape(userInput);
// Result: "Hello \\[World\\]! Price: \\$100"

// Practical use in search
function highlightText(text, searchTerm) {
  const safeSearchTerm = RegExp.escape(searchTerm);
  const regex = new RegExp(`(${safeSearchTerm})`, 'gi');
  return text.replace(regex, '<mark>$1</mark>');
}

const result = highlightText(
  "Price: $50.00 (promotion)",
  "$50.00"
);
// "<mark>$50.00</mark>" correctly highlighted

Float16Array

New typed array for 16-bit floats, ideal for ML and graphics.

// Float16Array - half the size of Float32Array
const float16Data = new Float16Array(4);
float16Data[0] = 1.5;
float16Data[1] = -2.25;
float16Data[2] = 3.14159; // Truncated to 16-bit precision
float16Data[3] = 0.0001;

console.log(float16Data.byteLength); // 8 bytes (vs 16 for Float32Array)

// Conversion between types
const float32 = new Float32Array([1.5, 2.5, 3.5, 4.5]);
const float16 = new Float16Array(float32); // Automatic conversion

// Useful for WebGL and Machine Learning
class NeuralNetworkLayer {
  constructor(inputSize, outputSize) {
    // Weights in Float16 save memory
    this.weights = new Float16Array(inputSize * outputSize);
    this.biases = new Float16Array(outputSize);
  }

  forward(input) {
    // Computation with reduced but sufficient precision
    const output = new Float16Array(this.biases.length);
    // ... implementation
    return output;
  }
}

Error.isError

Native verification if a value is really an Error object.

// Before - manual check (fails with objects from other realms)
function isErrorOld(value) {
  return value instanceof Error;
}

// Now - native method (works across realms)
console.log(Error.isError(new Error('test'))); // true
console.log(Error.isError(new TypeError('test'))); // true
console.log(Error.isError(new RangeError('test'))); // true
console.log(Error.isError({ message: 'fake error' })); // false
console.log(Error.isError(null)); // false

// Practical use in error handling
function handleResponse(result) {
  if (Error.isError(result)) {
    logger.error('Operation failed:', result.message);
    return { success: false, error: result.message };
  }
  return { success: true, data: result };
}

// Works with errors from iframes/workers
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const IframeError = iframe.contentWindow.Error;
const iframeError = new IframeError('from iframe');

console.log(iframeError instanceof Error); // false (different realms)
console.log(Error.isError(iframeError)); // true (works!)

Atomics.pause

New primitive for low-level synchronization in Workers.

// Atomics.pause - useful for efficient spin-waiting
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);

// Worker waiting for value
function waitForValue(expected) {
  while (Atomics.load(sharedArray, 0) !== expected) {
    // Pause allows CPU to optimize spin-wait
    Atomics.pause();
  }
  return sharedArray[0];
}

// Main thread signaling
function signalWorker(value) {
  Atomics.store(sharedArray, 0, value);
  Atomics.notify(sharedArray, 0);
}

// Spinlock implementation
class SpinLock {
  constructor(buffer, index) {
    this.array = new Int32Array(buffer);
    this.index = index;
  }

  acquire() {
    while (Atomics.compareExchange(this.array, this.index, 0, 1) !== 0) {
      Atomics.pause(); // CPU hint to wait
    }
  }

  release() {
    Atomics.store(this.array, this.index, 0);
  }
}

WebAssembly Memory64

Support for 64-bit memory for WebAssembly.

// WebAssembly with 64-bit memory
const memory64 = new WebAssembly.Memory({
  initial: 1,
  maximum: 65536, // Up to 4TB of memory
  index: 'i64', // 64-bit indexing
});

// Allows addressing more than 4GB of memory
// Useful for big data and ML applications

// Example usage with WASM module
const wasmCode = await fetch('/module.wasm');
const wasmBuffer = await wasmCode.arrayBuffer();

const wasmModule = await WebAssembly.instantiate(wasmBuffer, {
  env: {
    memory: memory64,
  },
});

// Process datasets larger than 4GB directly in WASM
const exports = wasmModule.instance.exports;
const result = exports.processLargeDataset(dataPointer, dataSize);

Improved AsyncLocalStorage

AsyncLocalStorage now uses AsyncContextFrame by default.

import { AsyncLocalStorage } from 'node:async_hooks';

const requestContext = new AsyncLocalStorage();

// Express/Fastify Middleware
function contextMiddleware(req, res, next) {
  const context = {
    requestId: crypto.randomUUID(),
    userId: req.user?.id,
    startTime: Date.now(),
  };

  requestContext.run(context, () => {
    next();
  });
}

// Logger that automatically includes context
class ContextLogger {
  log(message, data = {}) {
    const ctx = requestContext.getStore();
    console.log(JSON.stringify({
      timestamp: new Date().toISOString(),
      requestId: ctx?.requestId,
      userId: ctx?.userId,
      message,
      ...data,
    }));
  }

  // New: Improved performance with AsyncContextFrame
  async logAsync(message) {
    // Context preserved even in complex async operations
    await someAsyncOperation();
    this.log(message);
  }
}

// Use anywhere in the code
const logger = new ContextLogger();

async function processOrder(orderId) {
  logger.log('Processing order', { orderId });

  // Even in nested callbacks, context is preserved
  await db.transaction(async (trx) => {
    logger.log('Starting transaction');
    await trx.insert('orders', { id: orderId });
    logger.log('Order inserted');
  });

  logger.log('Order processed');
}

Performance improvements:

Operation Node.js 22 Node.js 24
getStore() ~150ns ~50ns
run() overhead ~500ns ~200ns
Nested contexts Linear Constant

Global URLPattern

The URLPattern API is now available globally.

// Before - required import
// import { URLPattern } from 'urlpattern-polyfill';

// Now - available globally
const pattern = new URLPattern({
  pathname: '/users/:userId/posts/:postId',
});

// URL matching
const url1 = 'https://api.example.com/users/123/posts/456';
const match = pattern.exec(url1);

console.log(match.pathname.groups);
// { userId: '123', postId: '456' }

// More complex patterns
const apiPattern = new URLPattern({
  protocol: 'https',
  hostname: '*.example.com',
  pathname: '/api/v:version/:resource{/:id}?',
});

const testUrls = [
  'https://api.example.com/api/v2/users',
  'https://api.example.com/api/v2/users/123',
  'https://cdn.example.com/api/v1/files/abc',
];

testUrls.forEach(url => {
  const result = apiPattern.exec(url);
  if (result) {
    console.log('Match:', {
      subdomain: result.hostname.groups[0],
      version: result.pathname.groups.version,
      resource: result.pathname.groups.resource,
      id: result.pathname.groups.id,
    });
  }
});

// Simple router using URLPattern
class PatternRouter {
  constructor() {
    this.routes = [];
  }

  add(method, patternString, handler) {
    this.routes.push({
      method,
      pattern: new URLPattern({ pathname: patternString }),
      handler,
    });
  }

  match(method, url) {
    for (const route of this.routes) {
      if (route.method !== method) continue;
      const match = route.pattern.exec(url);
      if (match) {
        return { handler: route.handler, params: match.pathname.groups };
      }
    }
    return null;
  }
}

const router = new PatternRouter();
router.add('GET', '/users/:id', getUserHandler);
router.add('POST', '/users', createUserHandler);
router.add('GET', '/posts/:postId/comments/:commentId?', getCommentsHandler);

npm 11

npm 11 brings significant improvements.

Installation Performance

# Faster installation with optimized cache
npm install

# Comparative benchmark (medium project ~500 deps)
# npm 10: 45 seconds
# npm 11: 28 seconds (-38%)

# New dependency resolution algorithm
# Less duplication, flatter tree

New Commands and Flags

# Improved dependency query
npm query ":root > :has([name^=@types/])"
# Lists all direct type dependencies

# Audit with more details
npm audit --format=json --detail

# Pack with preview
npm pack --dry-run --json
# Shows exactly what would be included

# Selective installation
npm install --omit=dev --omit=optional --omit=peer

# Improved integrity check
npm doctor --fix
# Fixes problems automatically when possible

Enhanced Workspaces

# Parallel execution in workspaces
npm run build --workspaces --parallel

# Filter by workspace
npm run test --workspace=packages/core --workspace=packages/utils

# Coordinated publishing
npm publish --workspaces --tag latest

# Dependencies between workspaces
npm install @myorg/utils --workspace=packages/core

Improved Native Test Runner

The Node.js test runner received significant improvements.

import { describe, it, before, after, mock } from 'node:test';
import assert from 'node:assert';

describe('UserService', () => {
  let db;
  let service;

  before(async () => {
    db = await createTestDatabase();
    service = new UserService(db);
  });

  after(async () => {
    await db.close();
  });

  // New: Subtests no longer need explicit await
  it('should create user', async (t) => {
    const user = await service.create({
      name: 'Test User',
      email: 'test@example.com',
    });

    // Automatically managed subtests
    t.test('should have valid id', () => {
      assert.ok(user.id);
      assert.match(user.id, /^[a-f0-9-]{36}$/);
    });

    t.test('should have timestamps', () => {
      assert.ok(user.createdAt instanceof Date);
      assert.ok(user.updatedAt instanceof Date);
    });
  });

  // New: Improved mocking
  it('should send welcome email', async (t) => {
    const sendEmail = t.mock.fn(async () => ({ sent: true }));

    // Module mock
    t.mock.module('../email-service.js', {
      namedExports: { sendEmail },
    });

    await service.create({ name: 'Test', email: 'test@test.com' });

    assert.strictEqual(sendEmail.mock.calls.length, 1);
    assert.strictEqual(
      sendEmail.mock.calls[0].arguments[0],
      'test@test.com'
    );
  });

  // New: Snapshot testing
  it('should format user correctly', async (t) => {
    const user = await service.getById('user-123');
    t.assert.snapshot(user);
  });
});

Running tests:

# Run all tests
node --test

# With coverage
node --test --experimental-test-coverage

# Watch mode
node --test --watch

# Filter by name
node --test --test-name-pattern="UserService"

# Custom reporter
node --test --test-reporter=spec

Undici 7

The default HTTP client has been updated to Undici 7.

import { request, fetch, Agent, Pool } from 'undici';

// Fetch with more control
const response = await fetch('https://api.example.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ query: 'test' }),
  // New: Granular timeout control
  signal: AbortSignal.timeout(5000),
});

// Optimized connection pool
const pool = new Pool('https://api.example.com', {
  connections: 10,
  pipelining: 6,
  keepAliveTimeout: 30000,
  keepAliveMaxTimeout: 60000,
});

// Requests using pool
const { statusCode, body } = await pool.request({
  path: '/users',
  method: 'GET',
});

const data = await body.json();

// Custom agent for retry
const retryAgent = new Agent({
  connect: {
    timeout: 10000,
  },
  // New: Automatic retry
  retry: {
    maxRetries: 3,
    minTimeout: 100,
    maxTimeout: 1000,
  },
});

// Improved streaming
const streamResponse = await fetch('https://api.example.com/stream', {
  dispatcher: retryAgent,
});

for await (const chunk of streamResponse.body) {
  process.stdout.write(chunk);
}

Deprecations and Removals

Some APIs have been deprecated or removed.

url.parse() Deprecated

// Deprecated
import { parse } from 'node:url';
const parsed = parse('https://example.com/path?query=1');

// Recommended: WHATWG URL API
const url = new URL('https://example.com/path?query=1');

console.log(url.hostname); // 'example.com'
console.log(url.pathname); // '/path'
console.log(url.searchParams.get('query')); // '1'

// Legacy code migration
// Before
const oldUrl = parse('https://user:pass@example.com:8080/path?q=1#hash');
console.log(oldUrl.auth); // 'user:pass'

// After
const newUrl = new URL('https://user:pass@example.com:8080/path?q=1#hash');
console.log(newUrl.username); // 'user'
console.log(newUrl.password); // 'pass'

tls.createSecurePair Removed

// Removed: tls.createSecurePair()

// Use tls.TLSSocket directly
import tls from 'node:tls';
import net from 'node:net';

const socket = net.connect(443, 'example.com');
const tlsSocket = new tls.TLSSocket(socket, {
  // options
});

Migrating from Node.js 22

Practical guide for upgrade.

Migration Checklist

# 1. Check current dependency versions
npm outdated

# 2. Update dependencies that may have issues
npm update

# 3. Check use of deprecated APIs
npx depcheck

# 4. Test with Node.js 24
nvm install 24
nvm use 24
npm test

# 5. Check native module compatibility
npm rebuild

# 6. Update engines in package.json
{
  "engines": {
    "node": ">=24.0.0"
  }
}

Important Breaking Changes

Changes that may affect your code:

  1. url.parse() now emits deprecation warning
  2. tls.createSecurePair has been removed
  3. Some experimental flags have been removed
  4. --experimental-specifier-resolution removed (use import maps)

Conclusion

Node.js 24 represents a significant evolution of the platform. With V8 13.6, we gain modern JavaScript features like RegExp.escape and Float16Array. npm 11 brings faster installations and enhanced workspaces. The native test runner is increasingly complete.

For those in production with Node.js 20 or 22, migration to 24 LTS (when available in October) will be smooth for most projects. Deprecations are few and well documented.

If you want to explore more about the modern JavaScript ecosystem, check out our article about ECMAScript 2025 and New JavaScript Features.

Let's go! 🦅

📚 Want to Master Node.js from Zero to Advanced?

This article showed Node.js 24 news, but to leverage the platform's full potential, you need solid fundamentals.

Complete Study Material

If you want to build a solid foundation in JavaScript to master Node.js:

Investment options:

  • 1x of $4.90 on card
  • or $4.90 at sight

👉 Learn About JavaScript Guide

💡 Solid JavaScript = Node.js mastered

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments