{"record":{"id":"4fcb47e081c61e99","repo":"facebook/react","slug":"a-rejected-promise-was-passed-to-react-without-a","errorCode":null,"errorMessage":"A rejected Promise was passed to React without a `reason` property. React threw a generic error from where the Promise was used to assist in identifying the problematic Promise. Make sure that instrumented Promises correctly set the `reason` property when setting `status` to `'rejected'`.","messagePattern":"A rejected Promise was passed to React without a `reason` property\\. React threw a generic error from where the Promise was used to assist in identifying the problematic Promise\\. Make sure that instrumented Promises correctly set the `reason` property when setting `status` to `'rejected'`\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/react-reconciler/src/ReactFiberThenable.js","lineNumber":231,"sourceCode":"    case 'fulfilled': {\n      // This could be a bad instrumentation that doesn't set .value.\n      // We're not type-checking since this is a hot path where you can\n      // track down easily when something becomes `undefined` unexpectedly.\n      const fulfilledValue: T = thenable.value;\n      return fulfilledValue;\n    }\n    case 'rejected': {\n      const rejectedError = thenable.reason;\n      checkIfUseWrappedInAsyncCatch(rejectedError);\n\n      // Rejected Promises are rarer so we're doing an extra type-check in\n      // case of a bad instrumentation that doesn't set .reason\n      // If we end up throwing `undefined` it becomes hard to track down\n      // where that throw originated because no callstack would exist.\n      // React would still have a Component stack but that could only be used\n      // as an approximation.\n      if (rejectedError === undefined && !('reason' in thenable)) {\n        throw new Error(\n          'A rejected Promise was passed to React without a `reason` property. ' +\n            'React threw a generic error from where the Promise was used to assist in identifying the problematic Promise. ' +\n            \"Make sure that instrumented Promises correctly set the `reason` property when setting `status` to `'rejected'`.\",\n        );\n      }\n\n      throw rejectedError;\n    }\n    default: {\n      if (typeof thenable.status === 'string') {\n        // Only instrument the thenable if the status if not defined. If\n        // it's defined, but an unknown value, assume it's been instrumented by\n        // some custom userspace implementation. We treat it as \"pending\".\n        // Attach a dummy listener, to ensure that any lazy initialization can\n        // happen. Flight lazily parses JSON when the value is actually awaited.\n        thenable.then(noop, noop);\n      } else {\n        // This is an uncached thenable that we haven't seen before.","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-reconciler/src/ReactFiberThenable.js#L213-L249","documentation":"trackUsedThenable synchronously unwraps instrumented thenables used by use()/Suspense. When thenable.status is 'rejected', React expects a .reason to rethrow. If reason is undefined AND the 'reason' property is missing entirely, rethrowing would produce an untrackable undefined error, so React throws this descriptive error at the point the Promise was consumed.","triggerScenarios":"Passing to use(), or throwing into a Suspense boundary, a manually-instrumented promise whose status was set to 'rejected' without also setting reason (the check is rejectedError === undefined && !('reason' in thenable)).","commonSituations":"Hand-rolled Suspense caches copying React's {status, value, reason} protocol incompletely; test mocks/stubs that pre-set promise.status; promise wrappers or serializers that clone an object and drop expando properties; libraries pre-rejecting promises for early exits.","solutions":["When instrumenting a rejected promise, always set both fields: p.status = 'rejected'; p.reason = error;","Better: do not pre-set status at all — pass the pending promise to React and let React instrument it via its own listeners","Audit wrappers/clones around promises for dropped expando properties (status/value/reason must survive)","Locate the offender from the stack: the throw site is where the badly-instrumented promise was consumed"],"exampleFix":"// before\nconst p = fetchThing();\np.status = 'rejected'; // manual instrumentation without reason\nuse(p);               // throws: rejected Promise without `reason`\n\n// after — either let React instrument it:\nconst p = fetchThing();\nuse(p); // React attaches its own listeners and sets status/value/reason\n\n// or set the full contract when instrumenting yourself:\nconst p = fetchThing();\np.status = 'rejected';\np.reason = new Error('fetchThing failed');","handlingStrategy":"validation","validationCode":"// Validate instrumentation before handing a promise to use()/Suspense\nfunction assertWellFormedThenable(p: Promise<any>): void {\n  if ((p as any).status === 'rejected' && !('reason' in p)) {\n    throw new Error('Promise instrumented as rejected without a `reason` property');\n  }\n}\n\n// assertWellFormedThenable(promise); use(promise);","typeGuard":"type Instrumented<T> = Promise<T> &\n  ({status: 'pending'} |\n   {status: 'fulfilled'; value: T} |\n   {status: 'rejected'; reason: unknown}); // reason required when rejected","tryCatchPattern":null,"preventionTips":["Never hand-set promise.status yourself — pass pending promises to React and let it instrument them","If you must instrument, always write the full contract: status plus value or reason","Audit promise wrappers/clones for dropped expando properties","When mocking promises in tests, reject via the executor instead of patching status"],"tags":["react","suspense","use","promise","thenable","instrumentation"],"backgroundTag":"suspense-thenable-instrumentation","analyzedSha":"eafeac097ba51e1eab809c07102126bd5f8e5425","analyzedAt":"2026-08-21T22:01:08.818Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}