dmtrKovalenko/fff · error

watch callback must be a function

Error message

watch callback must be a function

What it means

watch() accepts either a bare callback or (options, callback); before doing anything else it verifies the resolved callback is a function. If you pass the arguments in the wrong order or omit the callback, it returns this error instead of crossing the FFI boundary.

Solutions

  1. Pass the callback as the last argument: finder.watch(options, cb) or finder.watch(cb)
  2. Verify the callback is a function before calling watch
  3. Check the watch() overload documentation for the exact argument order you are using

Example fix

// before
finder.watch({ pattern: '*.rs' }); // missing callback
// after
finder.watch({ pattern: '*.rs' }, (events) => { console.log(events); });
Defensive patterns

Strategy: validation

Validate before calling

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

Type guard

function isFn(x) { return typeof x === 'function'; }

Try / catch

const res = finder.watch(opts, cb); if (!res.ok) { /* res.error === 'watch callback must be a function' -> fix call site */ }

Prevention

When it happens

Trigger: Calling finder.watch() with only an options object, passing the callback as the first argument when a pattern is also given (argument order confusion), or passing a non-function (string/undefined) where the callback belongs.

Common situations: Refactoring from watch(cb) to watch({pattern}, cb) or vice versa and leaving arguments swapped; TypeScript-less call sites where the signature is unchecked.

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/e7c854101e48cc84. Report an issue: GitHub.

Appendix: source

Thrown at packages/fff-bun/src/finder.ts:678

    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 trampoline = this.ensureWatchTrampoline(guard.value);
    if (!trampoline.ok) return trampoline;

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

    // No startup race: the threadsafe trampoline only runs on the JS event
    // loop, so this synchronous set always precedes the first routing lookup.
    const watchId = result.value;
    this.watchHandlers.set(watchId, callback);

    return { ok: true, value: () => this.unwatchById(watchId) };
  }

View on GitHub (pinned to 7f8537e70f)