Skip to content

fix(jest-mock): restore withImplementation after errors - #16219

Open
fallintoplace wants to merge 3 commits into
jestjs:mainfrom
fallintoplace:fix/withimplementation-restore-on-error
Open

fix(jest-mock): restore withImplementation after errors#16219
fallintoplace wants to merge 3 commits into
jestjs:mainfrom
fallintoplace:fix/withimplementation-restore-on-error

Conversation

@fallintoplace

Copy link
Copy Markdown

Summary

Fixes mockFn.withImplementation(...) so it restores the previous mock state when the callback throws synchronously or rejects asynchronously.

Closes #16218.

Motivation

withImplementation is documented as a temporary override, but the current implementation only restores state on the normal synchronous path and on fulfilled promises. That leaves the temporary implementation installed after exceptional exits.

How this works

  • restore the previous mock state in a shared helper
  • restore immediately when the callback throws
  • adopt thenables through the environment Promise and restore on both fulfillment and rejection
  • add regressions for sync throws and async rejections, including restoration of queued mockImplementationOnce state and whenCalledWith fallback behavior

Validation

  • yarn jest packages/jest-mock/src/__tests__/index.test.ts --runInBand
  • yarn jest packages/jest-mock/src/__tests__ --runInBand
  • yarn build:ts
  • yarn typecheck:tests

@netlify

netlify Bot commented May 25, 2026

Copy link
Copy Markdown

Deploy Preview for jestjs ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 6bbc503
🔍 Latest deploy log https://app.netlify.com/projects/jestjs/deploys/6a92a6b316850d000878a375
😎 Deploy Preview https://deploy-preview-16219--jestjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the require-changelog If a PR does requires a changelog entry label May 25, 2026
@fallintoplace
fallintoplace marked this pull request as ready for review May 25, 2026 22:47
@pkg-pr-new

pkg-pr-new Bot commented May 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

babel-jest

npm i https://pkg.pr.new/babel-jest@16219

babel-plugin-jest-hoist

npm i https://pkg.pr.new/babel-plugin-jest-hoist@16219

babel-preset-jest

npm i https://pkg.pr.new/babel-preset-jest@16219

create-jest

npm i https://pkg.pr.new/create-jest@16219

@jest/diff-sequences

npm i https://pkg.pr.new/@jest/diff-sequences@16219

expect

npm i https://pkg.pr.new/expect@16219

@jest/expect-utils

npm i https://pkg.pr.new/@jest/expect-utils@16219

jest

npm i https://pkg.pr.new/jest@16219

jest-changed-files

npm i https://pkg.pr.new/jest-changed-files@16219

jest-circus

npm i https://pkg.pr.new/jest-circus@16219

jest-cli

npm i https://pkg.pr.new/jest-cli@16219

jest-config

npm i https://pkg.pr.new/jest-config@16219

@jest/console

npm i https://pkg.pr.new/@jest/console@16219

@jest/core

npm i https://pkg.pr.new/@jest/core@16219

@jest/create-cache-key-function

npm i https://pkg.pr.new/@jest/create-cache-key-function@16219

jest-diff

npm i https://pkg.pr.new/jest-diff@16219

jest-docblock

npm i https://pkg.pr.new/jest-docblock@16219

jest-each

npm i https://pkg.pr.new/jest-each@16219

@jest/environment

npm i https://pkg.pr.new/@jest/environment@16219

jest-environment-jsdom

npm i https://pkg.pr.new/jest-environment-jsdom@16219

@jest/environment-jsdom-abstract

npm i https://pkg.pr.new/@jest/environment-jsdom-abstract@16219

jest-environment-node

npm i https://pkg.pr.new/jest-environment-node@16219

@jest/expect

npm i https://pkg.pr.new/@jest/expect@16219

@jest/fake-timers

npm i https://pkg.pr.new/@jest/fake-timers@16219

@jest/get-type

npm i https://pkg.pr.new/@jest/get-type@16219

@jest/globals

npm i https://pkg.pr.new/@jest/globals@16219

jest-haste-map

npm i https://pkg.pr.new/jest-haste-map@16219

jest-jasmine2

npm i https://pkg.pr.new/jest-jasmine2@16219

jest-leak-detector

npm i https://pkg.pr.new/jest-leak-detector@16219

jest-matcher-utils

npm i https://pkg.pr.new/jest-matcher-utils@16219

jest-message-util

npm i https://pkg.pr.new/jest-message-util@16219

jest-mock

npm i https://pkg.pr.new/jest-mock@16219

@jest/pattern

npm i https://pkg.pr.new/@jest/pattern@16219

jest-phabricator

npm i https://pkg.pr.new/jest-phabricator@16219

jest-regex-util

npm i https://pkg.pr.new/jest-regex-util@16219

@jest/reporters

npm i https://pkg.pr.new/@jest/reporters@16219

jest-resolve

npm i https://pkg.pr.new/jest-resolve@16219

jest-resolve-dependencies

npm i https://pkg.pr.new/jest-resolve-dependencies@16219

jest-runner

npm i https://pkg.pr.new/jest-runner@16219

jest-runtime

npm i https://pkg.pr.new/jest-runtime@16219

@jest/schemas

npm i https://pkg.pr.new/@jest/schemas@16219

jest-snapshot

npm i https://pkg.pr.new/jest-snapshot@16219

@jest/snapshot-utils

npm i https://pkg.pr.new/@jest/snapshot-utils@16219

@jest/source-map

npm i https://pkg.pr.new/@jest/source-map@16219

@jest/test-result

npm i https://pkg.pr.new/@jest/test-result@16219

@jest/test-sequencer

npm i https://pkg.pr.new/@jest/test-sequencer@16219

@jest/transform

npm i https://pkg.pr.new/@jest/transform@16219

@jest/types

npm i https://pkg.pr.new/@jest/types@16219

jest-util

npm i https://pkg.pr.new/jest-util@16219

jest-validate

npm i https://pkg.pr.new/jest-validate@16219

jest-watcher

npm i https://pkg.pr.new/jest-watcher@16219

jest-worker

npm i https://pkg.pr.new/jest-worker@16219

pretty-format

npm i https://pkg.pr.new/pretty-format@16219

commit: 6bbc503

@github-actions

Copy link
Copy Markdown

This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 30 days.

@github-actions github-actions Bot added the Stale label Aug 23, 2026
Comment thread packages/jest-mock/src/index.ts Outdated
throw error;
}

if (isPromise(returnedValue)) {

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.

isPromise(returnedValue) still runs outside the restoration try block. isPromise reads .then. If that property access throws, the error escapes before restore() and leaves the temporary implementation installed. The documentation explicitly supports callbacks that return thenables, so reading .then is part of this documented code path.

const mock = jest.fn(() => 'outside callback');

const thenable = Object.defineProperty({}, 'then', {
  get() {
    throw new Error('boom');
  },
}) as Promise<unknown>;

expect(() =>
  mock.withImplementation(() => 'inside callback', () => thenable),
).toThrow('boom');

expect(mock()).toBe('outside callback'); // Receives "inside callback"

Could the try block also cover isPromise(returnedValue) and the Promise.resolve(...).then(...) setup? I reproduced this on the PR head 55ce3234b303562ceb63b41ad44a8832d9b0ba61 and after applying the PR to current main be425a0b0e3bd60a74e4a7e350aa38c63a2d25ef. Moving those operations inside the try restores the original implementation. Locally, all 261 jest-mock tests across 6 suites pass.

@fallintoplace
fallintoplace force-pushed the fix/withimplementation-restore-on-error branch from 55ce323 to 6bbc503 Compare August 29, 2026 09:30
@github-actions github-actions Bot removed the Stale label Aug 29, 2026

@soltonigiri soltonigiri 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.

Thanks for fixing the restoration issue. I rechecked 6bbc5036, and an exception thrown while reading .then no longer skips restore(). I left two inline comments: the withImplementation tests don't exercise the changed source, and the changelog entry is under the released 30.5.0 section.

Comment thread CHANGELOG.md
- `[jest-runtime]` Throw `ERR_REQUIRE_CYCLE_MODULE` like Node when a CommonJS module `require()`s an ES module that is still being loaded, instead of evaluating the module a second time ([#16366](https://github.com/jestjs/jest/pull/16366))
- `[jest-runtime]` Key builtin modules in the ESM registry by one canonical specifier ([#16341](https://github.com/jestjs/jest/pull/16341))
- `[jest-runtime]` `import.meta.resolve()` for a builtin uses its `node:` specifier ([#16341](https://github.com/jestjs/jest/pull/16341))
- `[jest-mock]` Restore `withImplementation(...)` after sync throws and async rejections ([#16219](https://github.com/jestjs/jest/pull/16219))

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.

30.5.0 is already out. Could you move this entry to ## main under Fixes? yarn check-changelog 16219 exits 1 because it expects the PR link there.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks a lot for the comments.

Are you a maintainer though? I feel like this PR is not getting any attention.

Once again, I appreciate the reviews.

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.

No, I'm not a maintainer, just a fellow contributor 😅 I don't have merge permissions.

This PR has been open for a while, so I just wanted to help get it into tip-top shape for when a maintainer gets a chance to review it.

Thanks for being so receptive to the feedback!

});

it('restores the previous implementation after the callback throws', () => {
const mock = jest

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.

These tests create the target mocks with the runner's global jest.fn, so those mocks execute packages/jest-mock/build/index.js instead of the package source imported by this unit test. A focused coverage run of the full file left the changed index.ts:947-978 block uncovered. The three new error cases exercise those lines when switched to moduleMocker.fn, but the synchronous-success and fulfilled-promise restore cases need the same change. Could all withImplementation target mocks use moduleMocker.fn?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

require-changelog If a PR does requires a changelog entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: mockFn.withImplementation does not restore after throw or rejection

2 participants