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
- Pass a function as the callback argument: finder.watch(pattern, options, cb) or finder.watch(cb).
- Check typeof callback === 'function' before calling watch when the callback is dynamic.
- If you only have an options object, add the callback explicitly instead of relying on the overload resolution.
- 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
- Always pass the callback last: watch(pattern?, options?, callback).
- Check typeof before calling when the callback is dynamic.
- Do not nest the callback inside the options object.
- Re-read the watch() overload docs after API upgrades.
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
- ignore_count > 0 but ignore is NULL
- No watch callback registered. Call fff_set_watch_callback…
- watch callback must be a function
- patterns array must have at least 1 element
- Path constraint must be relative to the workspace
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)