Skip to content

fix: show a clear error when an unsupported Node.js version crashes the CLI - #3995

Open
michaelmalave wants to merge 4 commits into
mainfrom
worker/2026-10-14-cli-node-version-guard-task-1
Open

michaelmalave wants to merge 4 commits into
mainfrom
worker/2026-10-14-cli-node-version-guard-task-1

Conversation

@michaelmalave

@michaelmalave michaelmalave commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On Node.js 18 or older, most Heroku CLI commands crash with the raw message SyntaxError: Invalid regular expression flags and no hint about what to do. A dependency (string-width@8, pulled in through @oclif/table) needs Node.js 20 or later, even though the CLI already declares engines >=20. This is Sentry issue CLI-5K7, about 380K events per day, all from Node 16/18.

With this change, when Node.js is below 20 and the crash is that specific error, the CLI shows a clear message instead:

› Error: Node.js v18.20.8 is not supported. The Heroku CLI requires Node.js >=20. Upgrade Node.js, or install the standalone Heroku CLI, which includes its own Node.js: https://devcenter.heroku.com/articles/heroku-cli

The "requires" part is read from engines.node in package.json when the error happens and shown as written, so it always matches what the CLI declares. If that file can't be read, the message says "a newer version of Node.js" instead.

It uses Code: HEROKU_UNSUPPORTED_NODE and the exit code stays 1. Commands that already work on Node 18 (about 109 of 360 core commands, such as --version, --help, auth:*, logs, run, ps:scale, config:unset) are untouched. Everything else is handled exactly as before.

This condition is now treated as an expected error, so it is reported to Honeycomb only and no longer to Sentry (the same mechanism as the non-interactive login error). Already-released versions 11.10 to 11.12 cannot get this fix, so Sentry volume from those installs continues until users update.

Only the exact "Invalid regular expression flags" crash is treated this way, so an ordinary bad-pattern error on an old Node still shows up in Sentry as before. If the small helper that builds this message ever fails to load (for example a stale build), the CLI falls back to showing the original error instead of losing it.

Implementation: a new dependency-free src/lib/check-node-version.ts; bin/run.js now uses oclif run/flush/handle (what execute() does internally) with a catch for this case; and a new rule in isExpectedError.

Type of Change

Breaking Changes (major semver update)

  • Add a ! after your change type to denote a change that breaks current behavior

Feature Additions (minor semver update)

  • feat: Introduces a new feature to the codebase

Patch Updates (patch semver update)

  • fix: Bug fix
  • deps: Dependency upgrade
  • revert: Revert a previous commit
  • chore: Change that does not affect production code
  • refactor: Refactoring existing code without changing behavior
  • test: Add/update/remove tests

Testing

Notes:

  • New unit tests pass; eslint, tsc and build pass.
  • npm run test:ci:unit: 2424 passing, 9 failing. The same 9 fail on pristine main (8 version:info tests from a marked-terminal/chalk module-resolution error, 1 domains test), unrelated to this change.

Steps:

  1. Run the CLI on Node 18.20.8: heroku apps shows the unsupported-Node message and exits 1.
  2. On Node 18.20.8, heroku --version and heroku auth:token still work (exit 0).

Screenshots (if applicable)

Related Issues

GitHub issue: N/A
GUS work item: N/A

…crashes the CLI on load

On Node < 20 most commands crash while loading dependencies that use the regex v flag (Sentry CLI-5K7). Replace execute() in bin/run.js with run/flush/handle and turn that specific SyntaxError into an actionable HEROKU_UNSUPPORTED_NODE error. Classify it as expected so it goes to Honeycomb only.
@michaelmalave
michaelmalave requested a review from a team as a code owner October 14, 2026 21:19
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:19 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:19 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:19 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:19 — with GitHub Actions Active
@github-actions github-actions Bot added the size/M Medium PR (code churn < 500) label Oct 14, 2026
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:27 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:27 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:27 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:27 — with GitHub Actions Active
… guard match

bin/run.js falls back to handle(error) if the guard helper import fails; isUnsupportedNodeCrash now matches only "Invalid regular expression flags", so runtime bad-pattern SyntaxErrors on Node <20 still reach Sentry.
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:32 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:32 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:32 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 14, 2026 21:32 — with GitHub Actions Active

@tlowrimore-heroku tlowrimore-heroku left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of questions:

  • for the MIN_NODE_MAJOR value, does this need to match the value of the engines.node property in package.json? If so, should we read that value from package.json, and let that be the single source of truth?
  • Assuming the answer to the first question is, “yes”, do we also need to account for minor versions? Recently, we ran into an issue while attempting to upgrade some OTel dependencies that required a minimum node version of 20.6. We didn’t update to 20.6 as it’s a breaking change. I ask as this is top-of-mind, and would likely need to be accounted for, to allow use of minor versions in the engines.node property.

…rted-Node message

The message now reads engines.node from package.json at runtime and shows it as written, falling back to generic wording if it can't be read. The crash check keeps its own constant, renamed FIRST_NODE_MAJOR_WITH_V_FLAG, since it reflects when Node began parsing the regex v flag rather than the supported range. The engines drift test is removed because nothing duplicates engines any more.
@michaelmalave
michaelmalave deployed to AcceptanceTests October 15, 2026 15:20 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 15, 2026 15:20 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 15, 2026 15:20 — with GitHub Actions Active
@michaelmalave
michaelmalave deployed to AcceptanceTests October 15, 2026 15:20 — with GitHub Actions Active

@tlowrimore-heroku tlowrimore-heroku left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@michaelmalave

Copy link
Copy Markdown
Contributor Author

Good questions, and they're addressed in 53ec204.

MIN_NODE_MAJOR is gone. It was doing two jobs: deciding whether a crash is the Node 18 v-flag error, and filling in the message. The first isn't the support policy at all. It marks when Node began parsing that regex flag, so it's now FIRST_NODE_MAJOR_WITH_V_FLAG. The second now reads engines.node from package.json when the error happens and shows it as written. A change like >=20.6 shows up in the message automatically, with no range parsing and no drift test (removed). If the file can't be read, the message falls back to generic wording and never hides the original error.

The guard isn't affected by minor-version floors: a Node 20.5 failure from a dependency that needs 20.6 is a different error and passes through unchanged.

This branch was successfully deployed

1 active deployment
AcceptanceTests — 53ec2046 Deployed Oct 15, 2026 by michaelmalave via acceptance (20.x, ubuntu-latest) #9479
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR (code churn < 500)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants