facebook/relay · error · Error

NormalizationEngine: Expected id of elements of field

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.

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.

Appendix: 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)