Upgrading
June 2, 2026 ยท View on GitHub
This document describes breaking changes and how to upgrade. For a complete list of changes including minor and patch releases, please refer to the changelog.
13.0.0
Cli
The Cli class is no longer exported from the package. It was used internally to represent an instance of the command-line program invoked via cucumber-js, and could be used to run Cucumber programmatically, but was poorly suited for this. To adapt, pivot to the runCucumber function from the JavaScript API.
Ambiguous colons in formats
When parsing a format specified via the CLI, where the formatter and the destination are delimited by a colon, Cucumber no longer tries to handle additional colons in paths on a best-effort basis. To adapt, use double quotes on both sides of the delimiter so there's no ambiguity:
| Before | After |
|---|---|
html:file://hostname/formatter/report.html | "html":"file://hostname/formatter/report.html" |
file://C:\custom\formatter | "file://C:\custom\formatter" |
Parallel runtime
The parallel runtime has been reimplemented and now uses worker threads instead of child processes for the parallel workers. This doesn't change any documented behaviour, but it does mean that where each worker used to get its own memory allocation, they now all share with the main thread. If you run into memory issues after upgrading, consider giving Node.js more memory or tuning other settings.
Global hooks
BeforeAll and AfterAll hooks are now always executed, even if other hooks have failed, instead of failing fast on the first hook failure. To adapt, ensure your hooks will still work with potentially incomplete state from previous hook failures.
12.0.0
publishQuiet
The publishQuiet option (or --publish-quiet on the CLI) was used to hide the banner suggesting to use Cucumber Reports. The banner has since been removed, so the option now does nothing. To adapt, remove the option from your configuration files and CLI commands (especially the latter, since the CLI will fail on unrecognised options).
11.0.0
parseGherkinMessageStream and PickleFilter
parseGherkinMessageStream was a way to process a stream of envelopes from Gherkin and resolve to an array of filtered, ordered pickle Ids. The PickleFilter class was used to provide a filter to the aforementioned function. These interfaces included internal implementation details from Cucumber which were difficult to assemble. To adapt, pivot to the loadSources function from the JavaScript API, or raise an issue if you feel your use case isn't catered for.
Runtime
The Runtime class was used internally to represent an instance of the serial test case runner. Its interface included internal implementation details from Cucumber which were difficult to assemble. To adapt, pivot to the runCucumber function from the JavaScript API, or raise an issue if you feel your use case isn't catered for.
10.0.0
Configuration files
Configuration files must now be one of our supported extensions (.json, .yaml, .yml, .cjs, .js, .mjs). JavaScript files are now loaded with the appropriate mechanism based on the file extension and package type. If you previously relied on our internal usage of require() to dynamically transpile, you'll instead need to transpile beforehand and point Cucumber at the transpiled output.
Module loading
Custom formatters and snippet syntaxes are now always loaded with await import(). If you previously relied on our internal usage of require() to dynamically transpile, you'll instead need to transpile beforehand and point Cucumber at the transpiled output.
If no support code is specified with either the import or require options, we'll now load files from the default paths with await import(). If you need the use of require() for your setup to work, you'll need to use that option explicitly.
Attachments in JSON formatter
Previously, string attachments were included as plain text in the JSON formatter output, where other attachments were Base64 encoded. This meant for consumers, it was ambiguous whether any attachment was Base64 encoded or not. Now, all attachments are Base64 encoded regardless of how they were initially attached.
9.0.0
Generator snippet interface
Generator step definitions were removed in 8.0.0; we've now removed the associated snippet interface too. So if you have some configuration like:
{
"formatOptions": {
"snippetInterface": "generator"
}
}
Then you'll need to change that value to one of synchronous, async-await, promise or callback.
8.0.0
Generator step definitions
Generator functions used in step definitions (function* with the yield keyword)
are not natively supported anymore with cucumber-js.
You may consider using async/await rather than generators.
You can still use generators as before but you need to add your own dependencies
to bluebird and is-generator. Cucumber-js will no display explicit error message
anymore in case you use a generator without wrapping it properly.
const isGenerator = require('is-generator')
const {coroutine} = require('bluebird')
const {setDefinitionFunctionWrapper} = require('@cucumber/cucumber')
setDefinitionFunctionWrapper(function (fn) {
if (isGenerator.fn(fn)) {
return coroutine(fn)
} else {
return fn
}
})
Accessing willBeRetried from hooks
In the argument passed to your After hook function, the result no longer has a willBeRetried property; this is now available at the top level of the object.
Using Cli programmatically
The Cli class is sometimes used to run Cucumber programmatically. We've had to make a few breaking changes:
getConfiguration,initializeFormattersandgetSupportCodeLibrarymethods are removed- The constructor object has two new required properties:
stderr- writable stream to which we direct warning/error output - you might just passprocess.stderrenv- environment variables from which we detect some configuration options - you might just passprocess.env
In general for programmatic running (including those removed methods) we'd advise switching to the new API which is designed for this purpose.
Deep requires
Previously, you could require anything directly from Cucumber's internals e.g. require('@cucumber/cucumber/lib/formatter/helpers'). As part of adding ESM support we've added subpath exports, which restricts where Node.js can resolve modules from within the package. Deep requires are still possible but in a more limited way e.g. no implicit resolving of /index.js with the above example. In a future release we'll remove the capability for deep requires entirely, so we'd advise addressing any instances in your code (here's an example). Everything you need should be available via the main entry point, but if something's missing please raise an issue.
Formatter and snippet paths
When providing the path to a custom formatter or snippet syntax:
- For relative paths, you now need to ensure it begins with a
.(this was already the case for custom formatters as of 7.0.0; snippet syntaxes are being changed to match) - For absolute paths, you now need to provide it as a valid
file://URL
CLI options
These CLI options have been removed:
--retryTagFilter- the correct option is--retry-tag-filter--predictable-ids- this was only used for internal testing
7.0.0
Package Name
Cucumber is now published at @cucumber/cucumber instead of cucumber. To upgrade, you'll need to remove the old package and add the new one:
$ npm rm cucumber
$ npm install --save-dev @cucumber/cucumber
You'll need to update any import/require statements in your support code to use the new package name.
(The executable is still cucumber-js though.)
Hooks
The result object passed as the argument to your After hook function has a different structure.
Previously in cucumber:
{
"sourceLocation": {
"uri": "features/example.feature",
"line": 7
},
"pickle": {...},
"result": {
"duration": 660000000,
"status": "failed",
"exception": {
"name": "AssertionError",
"message": "...",
"showDiff": false,
"stack": "..."
},
"retried": true
}
}
Now in @cucumber/cucumber:
{
"gherkinDocument": {...}, // schema: https://github.com/cucumber/common/blob/messages/v16.0.1/messages/jsonschema/GherkinDocument.json
"pickle": {...}, // schema: https://github.com/cucumber/common/blob/messages/v16.0.1/messages/jsonschema/Pickle.json
"testCaseStartedId": "[uuid]",
"result": {
"status": "FAILED", // one of: UNKNOWN, PASSED, SKIPPED, PENDING, UNDEFINED, AMBIGUOUS, FAILED
"message": "...", // includes stack trace
"duration": {
"seconds": "0",
"nanos": 660000000
}
}
}
Formatters
The underlying event/data model for cucumber-js is now cucumber-messages, a shared standard across all official Cucumber implementations. This replaces the old "event protocol".
If you maintain any custom formatters, you'll need to refactor them to work with the new model. The basics of a Formatter class are the same, and the EventDataCollector is still there to help you with tracking down data, but the names of events and shape of their data is different. It's worth checking out the implementations of the built-in formatters if you need a pointer.
We now support referring to custom formatters on the path by module/package name, for example:
$ cucumber-js --format @cucumber/pretty-formatter
This does mean that if you want to point to a local formatter implementation (i.e. not a Node module) then you should ensure it's a relative path starting with ./.
Parallel
The parallel mode previously used problematic "master"/"slave" naming that we've dropped in favour of "coordinator" and "worker". This is mostly an internal detail, but is also reflected in the names of some environment variables you might be using:
CUCUMBER_TOTAL_SLAVESis nowCUCUMBER_TOTAL_WORKERSCUCUMBER_SLAVE_IDis nowCUCUMBER_WORKER_ID
TypeScript
(You can skip this part if you don't use TypeScript in your projects.)
Where before we relied on the community-authored @types/cucumber package, Cucumber is now built with TypeScript and as such includes its own typings, so you can drop your dependency on the separate package:
$ npm rm @types/cucumber
There are a few minor differences to be aware of:
- The type for data tables was named
TableDefinition- it's now namedDataTable Worldwas typed as an interface, but it's actually a class - you shouldextendit when building a custom formatter
Also, your tsconfig.json should have the resolveJsonModule compiler option switched on. Other than that, a pretty standard TypeScript setup should work as expected.
Timeouts
You can no longer call setDefaultTimeout from within other support code e.g. a step, hook or your World class; it should be called globally.