angular/components · warning

trackBy must be a function, but received ${JSON.stringify(fn

Error message

trackBy must be a function, but received ${JSON.stringify(fn)}.

What it means

The CDK data-table's `trackBy` input must be a `TrackByFunction<T>`. In dev mode, assigning a non-function truthy value logs this warning (the value is stored anyway, and Angular will then break at runtime when it tries to call it). The library warns instead of throwing so that production builds with `ngDevMode` stripped are unaffected.

Source

Thrown at src/cdk/table/table.ts:500

    }

    return this._cellRoleInternal;
  }
  private _cellRoleInternal: string | null | undefined = undefined;

  /**
   * Tracking function that will be used to check the differences in data changes. Used similarly
   * to `ngFor` `trackBy` function. Optimize row operations by identifying a row based on its data
   * relative to the function to know if a row should be added/removed/moved.
   * Accepts a function that takes two parameters, `index` and `item`.
   */
  @Input()
  get trackBy(): TrackByFunction<T> {
    return this._trackByFn;
  }
  set trackBy(fn: TrackByFunction<T>) {
    if ((typeof ngDevMode === 'undefined' || ngDevMode) && fn != null && typeof fn !== 'function') {
      console.warn(`trackBy must be a function, but received ${JSON.stringify(fn)}.`);
    }
    this._trackByFn = fn;
  }
  private _trackByFn!: TrackByFunction<T>;

  /**
   * The table's source of data, which can be provided in three ways (in order of complexity):
   *   - Simple data array (each object represents one table row)
   *   - Stream that emits a data array each time the array changes
   *   - `DataSource` object that implements the connect/disconnect interface.
   *
   * If a data array is provided, the table must be notified when the array's objects are
   * added, removed, or moved. This can be done by calling the `renderRows()` function which will
   * render the diff since the last table render. If the data array reference is changed, the table
   * will automatically trigger an update to the rows.
   *
   * When providing an Observable stream, the table will trigger an update automatically when the
   * stream emits a new array of data.

View on GitHub (pinned to 0411926e7d)

Solutions

  1. Bind an actual function reference: `trackBy` should receive `(index, item) => ...` or a method reference.
  2. Remove any `()` call or quotes in the template binding so the function itself is passed.
  3. Verify the assigned value with `typeof` before setting it if it comes from dynamic code.

Example fix

<!-- before -->
<table cdk-table [dataSource]="data" [trackBy]="'trackById'"></table>

<!-- after -->
<table cdk-table [dataSource]="data" [trackBy]="trackById"></table>
// component: trackById(index: number, row: Row) { return row.id; }
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof trackById !== 'function') {
  throw new TypeError('trackBy must be a function');
}
table.trackBy = trackById;

Type guard

function isTrackByFn<T>(v: unknown): v is TrackByFunction<T> {
  return typeof v === 'function';
}

Prevention

When it happens

Trigger: Binding something other than a function to `trackBy`, e.g. `trackBy="myTrackById"` in a template (string, not function reference), `trackBy={id: ...}` object, or calling it: `trackBy="trackById()"`.

Common situations: Copy-pasting `trackBy` usage from `*ngFor` examples where a string worked differently, forgetting the `this.`/method reference, or binding an observable-mapped value that isn't a function.

Related errors


AI-assisted analysis of angular/components@0411926e7d (2026-08-31). Data as JSON: /api/errors/1f7dc48185f7b784. Report an issue: GitHub.