angular/angular · error · Error

Task is missing scheduleFn.

Error message

Task is missing scheduleFn.

What it means

When a task is scheduled through a zone delegate chain that does not handle scheduling itself, zone.js falls back to task.scheduleFn; for microTasks it drains the microtask queue instead. If the task has no scheduleFn and is not a microTask (i.e. macroTask or eventTask), there is no way to actually start it, so scheduleTask throws 'Task is missing scheduleFn.' This almost always means a raw Task object was passed to scheduleTask instead of being created via the helper APIs.

Source

Thrown at packages/zone.js/lib/zone-impl.ts:1313

      let returnTask: ZoneTask<any> = task as ZoneTask<any>;
      if (this._scheduleTaskZS) {
        if (this._hasTaskZS) {
          returnTask._zoneDelegates!.push(this._hasTaskDlgtOwner!);
        }
        returnTask = this._scheduleTaskZS.onScheduleTask!(
          this._scheduleTaskDlgt!,
          this._scheduleTaskCurrZone!,
          targetZone,
          task,
        ) as ZoneTask<any>;
        if (!returnTask) returnTask = task as ZoneTask<any>;
      } else {
        if (task.scheduleFn) {
          task.scheduleFn(task);
        } else if (task.type == microTask) {
          scheduleMicroTask(<MicroTask>task);
        } else {
          throw new Error('Task is missing scheduleFn.');
        }
      }
      return returnTask;
    }

    invokeTask(targetZone: ZoneImpl, task: Task, applyThis: any, applyArgs?: any[]): any {
      return this._invokeTaskZS
        ? this._invokeTaskZS.onInvokeTask!(
            this._invokeTaskDlgt!,
            this._invokeTaskCurrZone!,
            targetZone,
            task,
            applyThis,
            applyArgs,
          )
        : task.callback.apply(applyThis, applyArgs);
    }

View on GitHub (pinned to 51cb07e980)

Solutions

  1. Use Zone.current.scheduleMacroTask(source, callback, data, scheduleFn, cancelFn) or scheduleEventTask(...) which wire scheduleFn correctly
  2. If building a Task by hand, supply the third-party scheduling step as scheduleFn: task.scheduleFn = (t) => { t.data!.handle = nativeSchedule(t.invoke) }
  3. For fire-and-forget microtasks, use Zone.current.scheduleMicroTask(source, callback, data) instead

Example fix

// before
const task = {type: 'macroTask', source: 'ipc', callback: onMsg} as any;
Zone.current.scheduleTask(task); // no scheduleFn -> throws

// after
const task = Zone.current.scheduleMacroTask('ipc', onMsg, null, (t) => {
  t.data = {...(t.data||{}), handle: ipcPort.once('msg', t.invoke)};
}, (t) => ipcPort.removeListener('msg', t.invoke));
Defensive patterns

Strategy: validation

Validate before calling

// never hand-build tasks; use the helper that sets scheduleFn
const task = Zone.current.scheduleMacroTask(
  'my-source', cb, null,
  (t) => { t.data = {handle: nativeSchedule(t.invoke)}; },
  (t) => nativeCancel(t.data!.handle),
);

Type guard

const isSchedulableTask = (t: any): boolean =>
  typeof t.scheduleFn === 'function' || t.type === 'microTask';

Prevention

When it happens

Trigger: Constructing a task manually (new (Zone as any).Task(...) or a hand-rolled object) and calling zone.scheduleTask(task) with scheduleFn unset; copying the Task shape from documentation but omitting the scheduleFn argument; calling scheduleEventTask/scheduleMacroTask with both customSchedule undefined while the delegate chain is empty — the normal helpers set scheduleFn, so hitting this usually means bypassing them.

Common situations: Library authors integrating custom async primitives (IPC, worker messages, custom timers) with zone.js; adapting old zone.js example code to newer typings; partially mocked Zone objects in tests where scheduleFn was stubbed away.

Related errors


AI-assisted analysis of angular/angular@51cb07e980 (2026-08-22). Data as JSON: /api/errors/1a50acc55d1334a2. Report an issue: GitHub.