facebook/relay · error · Error

NormalizationEngine: Expected record `${parentID}` to exist.

Error message

NormalizationEngine: Expected record `${parentID}` to exist.

What it means

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.

Source

Thrown at packages/relay-runtime/store/NormalizationEngine.js:607

    for (let i = 0; i < placeholders.length; i++) {
      const placeholder = placeholders[i];
      const {label, path} = placeholder;
      const pathKey = path.map(String).join('.');
      const key = makeKey(label, pathKey);
      this._placeholders.set(key, placeholder);

      // Cache parent record for @stream concurrent modification detection
      // and for @defer handle field replay
      let parentID: string;
      if (placeholder.kind === 'stream') {
        parentID = placeholder.parentID;
      } else {
        parentID = placeholder.selector.dataID;
      }

      const parentRecord = source.get(parentID);
      if (parentRecord == null) {
        throw err(
          'NormalizationEngine: Expected record `' + parentID + '` to exist.',
        );
      }

      const parentPayloads = (fieldPayloads ?? []).filter(
        (fieldPayload: HandleFieldPayload) => {
          const fieldID = generateClientID(
            fieldPayload.dataID,
            fieldPayload.fieldKey,
          );
          return fieldPayload.dataID === parentID || fieldID === parentID;
        },
      );

      const previousEntry = this._parentRecords.get(parentID);
      if (previousEntry != null) {
        const nextRecord = RelayModernRecord.update(
          previousEntry.record,

View on GitHub (pinned to 668b1b85e0)

Solutions

  1. Normalize the base response into the same record source before processing defer/stream followups
  2. Ensure the dataID used in the followup/defer selector matches the root/payload id actually normalized
  3. Check that garbage collection isn't running while multipart payloads are still in flight (retain queries)
  4. Log placeholder.parentID / selector.dataID and confirm source.get(dataID) exists before normalizing followups

Example fix

// before
followupResponses.forEach(r => engine.normalize(r)); // source empty
// after
engine.normalize(baseResponse); // writes parent records
followupResponses.forEach(r => engine.normalize(r));
Defensive patterns

Strategy: validation

Validate before calling

const parentID = placeholder.kind === 'stream' ? placeholder.parentID : placeholder.selector.dataID;
if (source.get(parentID) == null) {
  throw new Error(`Parent record ${parentID} missing before registering placeholder; normalize base response first`);
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of facebook/relay@668b1b85e0 (2026-09-02). Data as JSON: /api/errors/aa696eb4cc80b7f6. Report an issue: GitHub.