Tech Bytes Logo

Tech Bytes Engineering

October 22, 2025

NODE.JS RELEASE

Node.js 15: npm 7, V8 8.6 Engine & Critical Breaking Changes

Major release featuring npm workspaces, Promise.any(), logical assignment operators, and unhandled rejection changes that will break your code

npm 7 Workspaces V8 8.6 Engine Breaking Changes
Dillip Chowdary

Dillip Chowdary

Senior Full Stack Engineer • 6+ years Node.js expertise

Node.js 15 release features including npm 7 workspaces and V8 8.6 engine updates
Node.js 15.0.0 brings revolutionary npm workspaces, V8 8.6 engine, and critical breaking changes

Why Node.js 15 Matters

Node.js 15 is a current release line (not LTS) that introduces game-changing features for modern JavaScript development. While it's superseded by newer versions, understanding its innovations helps appreciate Node.js evolution.

Key Insight: This release marked a major shift with npm 7's workspace support, making monorepo development native to Node.js, and critical behavior changes around promise rejection handling that caught many developers off guard.

Released in October 2020, Node.js 15.0.0 brought npm 7, V8 JavaScript engine 8.6, experimental QUIC support, and several breaking changes that required careful migration planning. Let's dive into what made this release significant.

1. npm 7: Workspaces & Peer Dependencies

The biggest feature in Node.js 15 was the inclusion of npm 7, a complete rewrite with monorepo support and automatic peer dependency installation.

npm Workspaces: Native Monorepo Support

npm workspaces allow you to manage multiple packages within a single repository, similar to Yarn Workspaces or Lerna, but built directly into npm.

package.json (Root) JSON
{
  "name": "my-monorepo",
  "version": "1.0.0",
  "private": true,
  "workspaces": [
    "packages/*",
    "apps/*"
  ],
  "scripts": {
    "test": "npm run test --workspaces",
    "build": "npm run build --workspaces --if-present"
  },
  "devDependencies": {
    "jest": "^27.0.0",
    "typescript": "^4.5.0"
  }
}
Monorepo Structure Tree
my-monorepo/
├── package.json
├── package-lock.json
├── node_modules/
├── packages/
│   ├── shared-utils/
│   │   ├── package.json
│   │   └── index.js
│   └── api-client/
│       ├── package.json
│       └── index.js
└── apps/
    ├── web-app/
    │   ├── package.json
    │   └── src/
    └── mobile-app/
        ├── package.json
        └── src/
Workspace Commands Bash
# Install dependencies for all workspaces
npm install

# Run script in specific workspace
npm run build --workspace=packages/shared-utils

# Run script in all workspaces
npm run test --workspaces

# Add dependency to specific workspace
npm install lodash --workspace=packages/api-client

# Add dependency to root (available to all)
npm install -D jest

Automatic Peer Dependencies Installation

Breaking Change Alert

npm 7 now installs peer dependencies by default. This was a major change from npm 6 where peer dependencies were only warned about but not installed.

package.json (Library) JSON
{
  "name": "my-react-library",
  "version": "1.0.0",
  "peerDependencies": {
    "react": "^17.0.0",
    "react-dom": "^17.0.0"
  },
  "peerDependenciesMeta": {
    "react-dom": {
      "optional": true
    }
  },
  "devDependencies": {
    "react": "^17.0.0",
    "react-dom": "^17.0.0"
  }
}

npm 6 Behavior

Peer dependencies were not installed, only warned about. Developers had to install them manually.

npm 7 Behavior

Peer dependencies are automatically installed unless they conflict with existing versions.

New package-lock.json Format

npm 7 introduced lockfile version 2, which is backward compatible with npm 6 but includes additional workspace and peer dependency metadata.

Lockfile Features

  • Deterministic dependency trees
  • Workspace symlink information
  • Peer dependency resolution metadata
  • Backward compatible with npm 6
V8 8.6 JavaScript engine new features including Promise.any and logical assignment operators
V8 8.6 brings modern JavaScript features like Promise.any(), String.replaceAll(), and logical assignment operators

2. V8 JavaScript Engine 8.6

Node.js 15 upgraded from V8 8.4 to V8 8.6, bringing several ES2021 features and performance improvements.

Promise.any() & AggregateError

Promise.any() resolves when any of the promises fulfill, or rejects with an AggregateError if all promises reject.

Promise.any() Example JavaScript
// Fastest API response wins
const fetchFromMultipleSources = async () => {
  const apiUrls = [
    'https://api1.example.com/data',
    'https://api2.example.com/data',
    'https://api3.example.com/data'
  ];

  try {
    // Returns the first successful response
    const data = await Promise.any(
      apiUrls.map(url => fetch(url).then(r => r.json()))
    );
    console.log('Fastest response:', data);
    return data;
  } catch (error) {
    // AggregateError contains all rejection reasons
    console.error('All APIs failed:', error.errors);
    throw new Error('No API available');
  }
};

// Use case: Redundant service calls
const getDataWithFallbacks = () => {
  return Promise.any([
    fetchFromPrimaryDB(),    // Try primary first
    fetchFromCacheServer(),  // Fallback to cache
    fetchFromBackupDB()      // Last resort
  ]);
};

Promise.all()

Waits for all to fulfill, rejects on first rejection

Promise.race()

Settles with first promise to settle (fulfill or reject)

Promise.any()

Fulfills with first fulfillment, rejects if all reject

String.prototype.replaceAll()

String.replaceAll() Example JavaScript
// Old way: Using regex or split/join
const oldWay = 'foo bar foo baz foo'.replace(/foo/g, 'qux');
console.log(oldWay); // 'qux bar qux baz qux'

// New way: Native replaceAll()
const newWay = 'foo bar foo baz foo'.replaceAll('foo', 'qux');
console.log(newWay); // 'qux bar qux baz qux'

// Real-world example: Template rendering
const template = 'Hello {{name}}, welcome to {{app}}!';
const rendered = template
  .replaceAll('{{name}}', 'Dillip')
  .replaceAll('{{app}}', 'TechBytes');

console.log(rendered); // 'Hello Dillip, welcome to TechBytes!'

// Sanitizing user input
const sanitizeSQL = (query) => {
  return query
    .replaceAll("'", "''")    // Escape single quotes
    .replaceAll(';', '\\;');   // Escape semicolons
};

const userInput = "O'Reilly; DROP TABLE users;";
console.log(sanitizeSQL(userInput));
// "O''Reilly\\; DROP TABLE users\\;"

Logical Assignment Operators

New operators that combine logical operations with assignment: &&=, ||=, and ??=.

Logical Assignment Operators JavaScript
// 1. AND Assignment (&&=)
// Only assigns if left side is truthy
let user = { name: 'Alice', age: 30 };
user.age &&= user.age + 1;  // age is truthy, so increment
console.log(user.age);      // 31

// Equivalent to:
if (user.age) {
  user.age = user.age + 1;
}

// 2. OR Assignment (||=)
// Assigns if left side is falsy
let config = { timeout: 0 };
config.timeout ||= 5000;     // 0 is falsy, assign default
console.log(config.timeout); // 5000

// Equivalent to:
config.timeout = config.timeout || 5000;

// 3. Nullish Coalescing Assignment (??=)
// Assigns only if left side is null or undefined
let options = { retry: 0, cache: null };
options.retry ??= 3;         // 0 is not nullish, don't assign
options.cache ??= true;      // null is nullish, assign
console.log(options.retry);  // 0 (unchanged)
console.log(options.cache);  // true

// Real-world use case: Default configuration
class APIClient {
  constructor(options = {}) {
    this.baseURL = options.baseURL;
    this.baseURL ??= 'https://api.example.com';

    this.timeout = options.timeout;
    this.timeout ??= 5000;

    this.retries = options.retries;
    this.retries ??= 3;
  }
}

// Usage
const api1 = new APIClient({ timeout: 0 });
console.log(api1.timeout);  // 0 (explicit 0 is preserved)

const api2 = new APIClient({});
console.log(api2.timeout);  // 5000 (default applied)

3. AbortController API (Stable)

AbortController moved from experimental to stable, enabling cancellation of async operations like HTTP requests and file operations.

AbortController with fetch() JavaScript
// 1. Basic fetch cancellation
const controller = new AbortController();
const signal = controller.signal;

// Set timeout for request
setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch('https://api.example.com/slow', { signal });
  const data = await response.json();
  console.log(data);
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Request cancelled');
  } else {
    console.error('Request failed:', error);
  }
}

// 2. Cancel on user action
const searchController = new AbortController();

document.getElementById('search').addEventListener('input', async (e) => {
  // Cancel previous search
  searchController.abort();

  // Create new controller for this search
  const newController = new AbortController();

  try {
    const results = await fetch(`/api/search?q=${e.target.value}`, {
      signal: newController.signal
    });
    displayResults(await results.json());
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('Search failed:', error);
    }
  }
});

// 3. Cancel multiple operations with same signal
const batchController = new AbortController();
const { signal } = batchController;

const operations = [
  fetch('/api/users', { signal }),
  fetch('/api/posts', { signal }),
  fetch('/api/comments', { signal })
];

// Cancel all operations
setTimeout(() => batchController.abort(), 3000);

try {
  const results = await Promise.all(operations);
  console.log('All succeeded:', results);
} catch (error) {
  console.log('Operations cancelled or failed');
}
AbortController with fs operations JavaScript
const fs = require('fs/promises');
const { AbortController } = require('abort-controller');

// Cancel long-running file read
const controller = new AbortController();
const { signal } = controller;

// Cancel after 10 seconds
setTimeout(() => controller.abort(), 10000);

try {
  const data = await fs.readFile('huge-file.txt', { signal });
  console.log('File read complete');
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('File read cancelled');
  } else {
    throw error;
  }
}

Critical Breaking Changes

1. Unhandled Promise Rejections Now Throw

BREAKING: Application Crashes

In Node.js 15, the default mode for unhandledRejection changed from warn to throw. Unhandled promise rejections now crash your application instead of just logging a warning.

Unhandled Rejection Behavior JavaScript
// ❌ This will CRASH your app in Node.js 15+
async function badCode() {
  throw new Error('Unhandled rejection!');
}

badCode(); // No .catch() = process exit

// ✅ FIX 1: Add .catch() handler
badCode().catch(error => {
  console.error('Caught error:', error);
});

// ✅ FIX 2: Use try/catch with await
async function goodCode() {
  try {
    await badCode();
  } catch (error) {
    console.error('Caught error:', error);
  }
}

// ✅ FIX 3: Global unhandled rejection handler (last resort)
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  // Log to error tracking service
  // Don't exit - let app continue (risky!)
});

// Real-world example: Express middleware
app.get('/api/data', async (req, res, next) => {
  try {
    const data = await fetchDataFromDB();
    res.json(data);
  } catch (error) {
    next(error); // Pass to error handler middleware
  }
});

2. Stream Constructor Changes

Readable and Writable streams now use stream.construct API internally. This may affect custom stream implementations.

3. Deprecated APIs Removed

Removed APIs JavaScript
// ❌ REMOVED: server.connections
const server = http.createServer();
console.log(server.connections); // undefined in Node.js 15

// ✅ USE: server.getConnections() callback
server.getConnections((err, count) => {
  console.log('Active connections:', count);
});

// ❌ REMOVED: node debug command
// node debug script.js  // No longer works

// ✅ USE: node inspect
// node inspect script.js

// ❌ REMOVED: REPL deprecated functions
// repl.memory()
// repl.turnOffEditorMode()
// repl.parseREPLKeyword()

4. util.inspect() String Length Default

util.inspect() Changes JavaScript
const util = require('util');

// Node.js 14: Default maxStringLength = Infinity (no truncation)
// Node.js 15: Default maxStringLength = 10000

const longString = 'a'.repeat(20000);

// ❌ Will be truncated to 10,000 chars in Node.js 15
console.log(util.inspect({ longString }));

// ✅ Explicitly set maxStringLength if needed
console.log(util.inspect({ longString }, {
  maxStringLength: Infinity  // No truncation
}));

console.log(util.inspect({ longString }, {
  maxStringLength: 50  // Truncate to 50 chars
}));

Experimental Features

QUIC Protocol Support

Node.js 15 introduced experimental QUIC support, the protocol underlying HTTP/3, for faster and more reliable network connections.

Enable QUIC Support Bash
# Compile Node.js with QUIC support
./configure --experimental-quic
make -j4

# Or use pre-built binary with flag
node --experimental-quic app.js

QUIC Benefits

  • Faster connection establishment (0-RTT for repeat connections)
  • Built-in encryption (TLS 1.3 by default)
  • Better loss recovery than TCP
  • Connection migration (switch networks without reconnecting)

N-API 7

N-API version 7 added new methods for working with ArrayBuffers and improved native addon compatibility.

N-API 7 Additions

  • napi_detach_arraybuffer()
  • napi_is_detached_arraybuffer()
  • • Improved performance for native modules

Migration Guide: Node.js 14 → 15

Pre-Migration Checklist

Step-by-Step Migration Bash
# Step 1: Update Node.js
nvm install 15
nvm use 15

# Step 2: Clear npm cache and node_modules
rm -rf node_modules package-lock.json
npm cache clean --force

# Step 3: Reinstall with npm 7
npm install

# Step 4: Check for peer dependency warnings
npm ls

# Step 5: Run tests with unhandled rejection detection
NODE_OPTIONS='--unhandled-rejections=strict' npm test

# Step 6: Fix all unhandled rejections before deploying
# Add .catch() to all promise chains
# Wrap async functions in try/catch

# Step 7: Update CI/CD Node version
# .github/workflows/ci.yml
# - uses: actions/setup-node@v2
#   with:
#     node-version: '15'

Performance Improvements

Node.js 15 performance benchmarks showing V8 8.6 improvements
V8 8.6 brings significant performance improvements in Promise handling and string operations

Promise Performance

~15%

Faster Promise.all() and async/await

String Operations

~20%

String.replaceAll() optimizations

npm Install

~30%

Faster with new lockfile format

Key Takeaways

Major Wins

  • npm 7 workspaces for monorepos
  • V8 8.6 with Promise.any()
  • Stable AbortController API
  • Logical assignment operators
  • Experimental QUIC support

Watch Out For

  • Unhandled rejections now crash apps
  • Peer dependencies auto-installed
  • Deprecated APIs removed
  • util.inspect() string truncation
  • Stream constructor changes

Conclusion

Node.js 15 represented a significant evolution in the Node.js ecosystem, bringing modern JavaScript features and professional-grade tooling with npm 7 workspaces. While the breaking changes required careful migration planning, the benefits—especially for monorepo projects—made it a worthwhile upgrade.

For new projects: Consider using LTS versions (Node.js 14, 16, 18, 20) for production stability.

Learning from Node.js 15: Understanding this release helps appreciate the evolution of Node.js and prepares you for similar changes in future major versions.

Stay Updated on Node.js & JavaScript

Get weekly Node.js, JavaScript & DevOps insights in your inbox