{"record":{"id":"aa696eb4cc80b7f6","repo":"facebook/relay","slug":"normalizationengine-expected-record-parentid-aa696e","errorCode":null,"errorMessage":"NormalizationEngine: Expected record `${parentID}` to exist.","messagePattern":"NormalizationEngine: Expected record `(.+?)` to exist\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/relay-runtime/store/NormalizationEngine.js","lineNumber":607,"sourceCode":"    for (let i = 0; i < placeholders.length; i++) {\n      const placeholder = placeholders[i];\n      const {label, path} = placeholder;\n      const pathKey = path.map(String).join('.');\n      const key = makeKey(label, pathKey);\n      this._placeholders.set(key, placeholder);\n\n      // Cache parent record for @stream concurrent modification detection\n      // and for @defer handle field replay\n      let parentID: string;\n      if (placeholder.kind === 'stream') {\n        parentID = placeholder.parentID;\n      } else {\n        parentID = placeholder.selector.dataID;\n      }\n\n      const parentRecord = source.get(parentID);\n      if (parentRecord == null) {\n        throw err(\n          'NormalizationEngine: Expected record `' + parentID + '` to exist.',\n        );\n      }\n\n      const parentPayloads = (fieldPayloads ?? []).filter(\n        (fieldPayload: HandleFieldPayload) => {\n          const fieldID = generateClientID(\n            fieldPayload.dataID,\n            fieldPayload.fieldKey,\n          );\n          return fieldPayload.dataID === parentID || fieldID === parentID;\n        },\n      );\n\n      const previousEntry = this._parentRecords.get(parentID);\n      if (previousEntry != null) {\n        const nextRecord = RelayModernRecord.update(\n          previousEntry.record,","sourceCodeStart":589,"sourceCodeEnd":625,"githubUrl":"https://github.com/facebook/relay/blob/668b1b85e06261aa3b58dabfc51f8b5524a70955/packages/relay-runtime/store/NormalizationEngine.js#L589-L625","documentation":"After normalizing a response, Relay registers @defer/@stream placeholders and must locate each placeholder's parent record in the mutable record source (`source.get(parentID)`). If that record is missing, the placeholder refers to data that was never written — typically a `dataID` mismatch between the payload normalization and the placeholder's selector, or a record that got GC'd/not committed. Relay throws rather than silently orphaning the incremental work.","triggerScenarios":"Calling normalize (via processResponse) on a response containing @defer/@stream placeholders whose parent selector.dataID / parentID does not exist in the record source — e.g. normalizing a followup payload against an empty record source, a base response that was never normalized, or IDs that changed between base and followup normalizations.","commonSituations":"Custom environments normalizing incremental payloads with a fresh/blank NormalizationInboundTextStorage, tests invoking _processDefer/_normalizeFollowup out of order, or store eviction/GC removing a parent record mid-multipart-response.","solutions":["Normalize the base response into the same record source before processing defer/stream followups","Ensure the dataID used in the followup/defer selector matches the root/payload id actually normalized","Check that garbage collection isn't running while multipart payloads are still in flight (retain queries)","Log placeholder.parentID / selector.dataID and confirm source.get(dataID) exists before normalizing followups"],"exampleFix":"// before\nfollowupResponses.forEach(r => engine.normalize(r)); // source empty\n// after\nengine.normalize(baseResponse); // writes parent records\nfollowupResponses.forEach(r => engine.normalize(r));","handlingStrategy":"validation","validationCode":"const parentID = placeholder.kind === 'stream' ? placeholder.parentID : placeholder.selector.dataID;\nif (source.get(parentID) == null) {\n  throw new Error(`Parent record ${parentID} missing before registering placeholder; normalize base response first`);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Process multipart payloads strictly in order against one record source","Retain queries so GC doesn't evict parents mid-stream","Keep dataIDs consistent between base and followup normalizations"],"tags":["relay","defer","normalization","store"],"backgroundTag":"relay-stream-normalization","analyzedSha":"668b1b85e06261aa3b58dabfc51f8b5524a70955","analyzedAt":"2026-09-02T19:57:20.783Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}