facebook/relay · error · Error

NormalizationEngine: Expected id of elements of field `${sto

Error message

NormalizationEngine: Expected id of elements of field `${storageKey}` to be strings.

What it means

Relay resolves the streamed item's ID in order: a custom getDataID(options), the ID recorded previously at that list index, or a generated client ID. All three paths failed to produce a string — typically because a custom getDataID returned a non-string (e.g. a number or undefined) for this item, so Relay cannot key the record in the store. Relay requires all record IDs to be strings.

Source

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

    const typeName = field.concreteType ?? (data as $FlowFixMe).__typename;
    if (typeof typeName !== 'string') {
      throw err(
        'NormalizationEngine: Expected @stream field `' +
          field.name +
          '` to have a __typename.',
      );
    }

    const getDataID = this._options.getDataID;
    const itemID =
      (typeof getDataID === 'function'
        ? getDataID(data as $FlowFixMe, typeName)
        : null) ??
      prevIDs?.[itemIndex] ??
      generateClientID(parentID, storageKey, itemIndex);
    if (typeof itemID !== 'string') {
      throw err(
        'NormalizationEngine: Expected id of elements of field `' +
          storageKey +
          '` to be strings.',
      );
    }

    const selector = createNormalizationSelector(field, itemID, variables);

    const nextParentRecord = RelayModernRecord.clone(parentRecord);
    const nextIDs = [...prevIDs];
    nextIDs[itemIndex] = itemID;
    RelayModernRecord.setLinkedRecordIDs(nextParentRecord, storageKey, nextIDs);
    this._parentRecords.set(parentID, {
      fieldPayloads,
      record: nextParentRecord,
    });

    const relayPayload = this._normalizeResponse(

View on GitHub (pinned to 668b1b85e0)

Solutions

  1. Make your custom getDataID always return a string, coercing numbers with String(id)
  2. Ensure every streamed item actually contains the field your getDataID reads
  3. Return null (not a non-string) from getDataID when the id can't be derived so Relay falls back to prevIDs/client IDs
  4. Log getDataID inputs during normalization to find which items yield non-string ids

Example fix

// before
const getDataID = (data) => data.id;
// after
const getDataID = (data) => (data.id != null ? String(data.id) : null);
Defensive patterns

Strategy: type-guard

Validate before calling

const data = payload.data as { id?: unknown };
if (typeof getDataID?.(data, typeName) !== 'string') {
  console.warn('getDataID returned non-string for streamed item', data);
}

Type guard

function hasStringId(d: unknown): d is { id: string } {
  return typeof (d as any)?.id === 'string' && (d as any).id.length > 0;
}

Prevention

When it happens

Trigger: Configuring the store with getDataID that returns a non-string (numeric ids, undefined when a field is missing) for items of the streamed field `storageKey`, causing `typeof itemID !== 'string'`.

Common situations: Apps with numeric (Int) database ids that implemented getDataID without String() conversion, ids nested under a different key than getDataID expects, or items where the id field is absent on some union members.

Related errors


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