{"record":{"id":"43eaf3bcfaa74180","repo":"jestjs/jest","slug":"unexpected-return-from-a-matcher-function-matcher","errorCode":null,"errorMessage":"Unexpected return from a matcher function.\nMatcher functions should return an object in the following format:\n  {message?: string | function, pass: boolean}\n'${matcherUtils.stringify(result)}' was returned","messagePattern":"Unexpected return from a matcher function\\.\nMatcher functions should return an object in the following format:\n  (.+?)\n'(.+?)' was returned","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/expect/src/index.ts","lineNumber":430,"sourceCode":"  stringMatching: stringNotMatching,\n};\n\nexpect.arrayContaining = arrayContaining;\nexpect.arrayOf = arrayOf;\nexpect.closeTo = closeTo;\nexpect.objectContaining = objectContaining;\nexpect.stringContaining = stringContaining;\nexpect.stringMatching = stringMatching;\n\nconst _validateResult = (result: any) => {\n  if (\n    typeof result !== 'object' ||\n    typeof result.pass !== 'boolean' ||\n    (result.message &&\n      typeof result.message !== 'string' &&\n      typeof result.message !== 'function')\n  ) {\n    throw new Error(\n      'Unexpected return from a matcher function.\\n' +\n        'Matcher functions should ' +\n        'return an object in the following format:\\n' +\n        '  {message?: string | function, pass: boolean}\\n' +\n        `'${matcherUtils.stringify(result)}' was returned`,\n    );\n  }\n};\n\nfunction assertions(expected: number): void {\n  const error = new ErrorWithStack(undefined, assertions);\n\n  setState({\n    expectedAssertionsNumber: expected,\n    expectedAssertionsNumberError: error,\n  });\n}\nfunction hasAssertions(...args: Array<unknown>): void {","sourceCodeStart":412,"sourceCodeEnd":448,"githubUrl":"https://github.com/jestjs/jest/blob/8e6d128e4a278059ecddecaa97400b04c8ae5fd9/packages/expect/src/index.ts#L412-L448","documentation":"Every matcher (built-in or from expect.extend) must return an object shaped like {pass: boolean, message?: string | (() => string)}. _validateResult runs after each matcher call and throws an Error if the return is missing, not an object, lacks a boolean `pass`, or has a `message` of the wrong type. This protects the dispatcher and diff renderer from malformed matcher output.","triggerScenarios":"Writing a custom matcher (via expect.extend) that returns undefined, a boolean, or an object without a `pass` field; or a matcher that returns {message: 123} (non-string/non-function message). The error shows the actual returned value via stringify to aid debugging.","commonSituations":"Forgetting `return` in a matcher arrow function so it returns undefined; returning `true/false` instead of `{pass, message}`; a matcher whose early-return path omits the message; refactoring a matcher and dropping the return shape.","solutions":["Ensure your matcher returns {pass: boolean, message: () => string} on every code path, including early returns.","If using an arrow expression body, wrap the body in a block with an explicit return, or return the object literal directly.","Use jest-matcher-utils' matcherHint/printExpected/printReceived to build the message thunk, and confirm `message` is a function (preferred) or string."],"exampleFix":"// before\nexpect.extend({\n  toBeWithin(received, floor, ceil) {\n    if (received >= floor && received <= ceil) return true; // WRONG: boolean\n    return false;\n  },\n});\n\n// after\nexpect.extend({\n  toBeWithin(received, floor, ceil) {\n    const pass = received >= floor && received <= ceil;\n    return {\n      pass,\n      message: () =>\n        `expected ${received} to be within [${floor}, ${ceil}]`,\n    };\n  },\n});","handlingStrategy":"validation","validationCode":"// Validate a matcher's return shape before registering it.\nfunction isValidMatcherResult(r) {\n  return (\n    r != null &&\n    typeof r === 'object' &&\n    typeof r.pass === 'boolean' &&\n    (r.message === undefined ||\n      typeof r.message === 'string' ||\n      typeof r.message === 'function')\n  );\n}\n\n// In a dev wrapper around expect.extend, sanity-check each matcher on a sample input.","typeGuard":"interface MatcherResult { pass: boolean; message?: string | (() => string); }\n\nfunction isMatcherResult(v: unknown): v is MatcherResult {\n  if (!v || typeof v !== 'object') return false;\n  const r = v as Record<string, unknown>;\n  if (typeof r.pass !== 'boolean') return false;\n  const m = r.message;\n  return m === undefined || typeof m === 'string' || typeof m === 'function';\n}","tryCatchPattern":"try {\n  const result = customMatcher.call(ctx, received, ...args);\n  if (!isMatcherResult(result)) {\n    throw new Error(`Custom matcher returned an invalid shape: ${JSON.stringify(result)}`);\n  }\n} catch (e) {\n  // report the matcher name and received args to fix the return shape\n}","preventionTips":["Always return {pass, message: () => string} from custom matchers on every path.","Use a block body with explicit return statements to avoid implicit undefined.","Write a tiny unit test for each custom matcher that asserts the result shape."],"tags":["expect","custom-matcher","expect-extend","api-misuse"],"backgroundTag":null,"analyzedSha":"8e6d128e4a278059ecddecaa97400b04c8ae5fd9","analyzedAt":"2026-08-10T18:11:27.960Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}