{"record":{"id":"a293660c1f05d70d","repo":"facebook/relay","slug":"normalizationengine-expected-the-parent-record","errorCode":null,"errorMessage":"NormalizationEngine: Expected the parent record `${parentID}` for @stream data to exist.","messagePattern":"NormalizationEngine: Expected the parent record `(.+?)` for @stream data to exist\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/relay-runtime/store/NormalizationEngine.js","lineNumber":488,"sourceCode":"    itemID: string,\n    itemIndex: number,\n    prevIDs: Array<?string>,\n    relayPayload: RelayResponsePayload,\n    storageKey: string,\n  } {\n    const {data} = response;\n    if (typeof data !== 'object') {\n      throw err(\n        'NormalizationEngine: Expected the GraphQL @stream payload `data` ' +\n          'value to be an object.',\n      );\n    }\n    const responseKey = field.alias ?? field.name;\n    const storageKey = getStorageKey(field, variables);\n\n    const parentEntry = this._parentRecords.get(parentID);\n    if (parentEntry == null) {\n      throw err(\n        'NormalizationEngine: Expected the parent record `' +\n          parentID +\n          '` for @stream data to exist.',\n      );\n    }\n    const {fieldPayloads, record: parentRecord} = parentEntry;\n\n    const prevIDs = RelayModernRecord.getLinkedRecordIDs(\n      parentRecord,\n      storageKey,\n    );\n    if (prevIDs == null) {\n      throw err(\n        'NormalizationEngine: Expected record `' +\n          parentID +\n          '` to have fetched field `' +\n          field.name +\n          '` with @stream.',","sourceCodeStart":470,"sourceCodeEnd":506,"githubUrl":"https://github.com/facebook/relay/blob/668b1b85e06261aa3b58dabfc51f8b5524a70955/packages/relay-runtime/store/NormalizationEngine.js#L470-L506","documentation":"Relay tracks parent records for each streamed field in `_parentRecords` when the initial items arrive. When a subsequent @stream item arrives, _normalizeStreamItem looks up the parent record by `path`-derived `parentID` to append/attach the new item. If no record was registered for that ID (parent response never normalized, path mismatch, or store cleared), Relay throws.","triggerScenarios":"An incremental @stream item arrives whose `path` points to a parent record that was never registered — e.g. the initial (non-incremental) response part was dropped or processed by a different store/Relay environment, the path in the payload doesn't match the query structure, or records were garbage-collected between the initial response and stream items.","commonSituations":"Multipart chunks routed to different Relay environments or a store reset between the initial response and streamed items; custom network layers reordering parts so a stream item is processed before its parent; server emitting paths inconsistent with the query's aliasing; test mocks emitting stream items without the base response.","solutions":["Ensure the initial response (containing the streamed field's base record) is fully normalized in the same Relay environment/store before incremental items are processed.","Verify the server's incremental `path` values match the query aliases/structure so Relay derives the correct parentID.","Check that no store reset/GC (e.g. environment.store.reset(), retention expiry) runs between the base response and the stream items.","Fix custom network layers to preserve part ordering (base response first, then incrementals) and route all parts to one environment.","In mocks/tests, always emit the complete initial response before any @stream incremental parts."],"exampleFix":"// before\nparts.forEach(part => processPart(part)); // stream items may arrive before base record\n// after\nconst [base, ...incrementals] = parts;\nprocessPart(base);\nincrementals.forEach(processPart); // parent record now registered","handlingStrategy":"validation","validationCode":"// Ensure base response was processed and parent record exists before stream items\nconst record = environment.getStore().getSource().get(parentIdFromPath(part.path));\nif (record == null) {\n  throw new Error('Parent record missing; process the base response first');\n}","typeGuard":"function parentRecordExists(environment, path) {\n  const id = path && path.length ? String(path[0]) : null;\n  return id != null && environment.getStore().getSource().get(id) != null;\n}","tryCatchPattern":"try {\n  normalizationEngine.processIncrementalResponse(response);\n} catch (e) {\n  if (String(e.message).includes('for @stream data to exist')) {\n    console.error('Parent record missing — base response dropped or store reset:', response.path);\n  } else throw e;\n}","preventionTips":["Process the initial response before any incremental parts, in the same Relay environment.","Avoid store resets / GC between the base response and streamed items.","Preserve multipart part ordering in custom network layers.","Verify server incremental paths match query aliases and structure.","In tests, always emit the full base response before @stream incrementals."],"tags":["relay","graphql","stream","store-consistency"],"backgroundTag":"missing-parent-record","analyzedSha":"668b1b85e06261aa3b58dabfc51f8b5524a70955","analyzedAt":"2026-09-02T19:57:20.783Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}