dmtrKovalenko/fff · error

watch callback must be a function

Error message

watch callback must be a function

What it means

The watch() method accepts a callback (with optional pattern and options), and the resolved callback argument must be a function. If the first/last argument resolved as the callback is not callable, watch() rejects the call before touching the native layer, since ffiWatch must register a JS callback to be invoked on file events.

Solutions

  1. Pass a function as the callback argument: finder.watch(pattern, options, cb) or finder.watch(cb).
  2. Check typeof callback === 'function' before calling watch when the callback is dynamic.
  3. If you only have an options object, add the callback explicitly instead of relying on the overload resolution.
  4. Review the watch() signature in finder.ts to confirm argument order for the overload you intend.

Example fix

// before
finder.watch({ ignore: ['node_modules'] }); // no callback
// after
finder.watch({ ignore: ['node_modules'] }, (events) => {
  console.log('watch events', events);
});
Defensive patterns

Strategy: validation

Validate before calling

if (typeof cb !== 'function') throw new TypeError('watch requires a callback function');

Type guard

function isWatchCallback(v: unknown): v is WatchBatchCallback {
  return typeof v === 'function';
}

Try / catch

const res = finder.watch(pattern, opts, cb);
if (!res.ok && res.error.includes('callback must be a function')) {
  throw new TypeError('Pass a function to watch(), got: ' + typeof cb);
}

Prevention

When it happens

Trigger: Calling finder.watch() with no arguments; passing an options object as the only argument without a callback (and no trailing callback); passing a non-function such as a string, an async wrapper misused as a value, or a callback stored under a different property name.

Common situations: Misreading the overload signature and passing (options) only; refactoring from an event-emitter style API and forgetting the callback argument; passing a callback property like { callback: fn } instead of fn itself.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of dmtrKovalenko/fff@7f8537e70f (2026-09-10). Data as JSON: /api/errors/c6157bea1339ef3b. Report an issue: GitHub.

Appendix: source

Thrown at packages/fff-node/src/finder.ts:658

    options?: WatchOptions,
  ): Result<WatchUnsubscribe>;
  watch(
    patternOrCallback: string | WatchBatchCallback,
    callbackOrOptions?: WatchBatchCallback | WatchOptions,
    maybeOptions?: WatchOptions,
  ): Result<WatchUnsubscribe> {
    // Overload shift: watch(cb, opts?) -> empty pattern = whole tree.
    const noPattern = typeof patternOrCallback === "function";
    const pattern = noPattern ? "" : patternOrCallback;
    const callback = noPattern
      ? patternOrCallback
      : (callbackOrOptions as WatchBatchCallback);
    const options = noPattern
      ? (callbackOrOptions as WatchOptions | undefined)
      : maybeOptions;

    if (typeof callback !== "function") {
      return err("watch callback must be a function");
    }

    const guard = this.ensureAlive();
    if (!guard.ok) return guard;

    const created = ffiWatch(guard.value, pattern, options?.ignore ?? [], callback);
    if (!created.ok) return created;

    const watchId = created.value;
    this.watchers.add(watchId);

    return {
      ok: true,
      value: () => {
        if (!this.watchers.delete(watchId)) return;
        if (this.handle !== null) ffiUnwatch(this.handle, watchId);
      },
    };

View on GitHub (pinned to 7f8537e70f)