{"record":{"id":"e7c854101e48cc84","repo":"dmtrKovalenko/fff","slug":"watch-callback-must-be-a-function","errorCode":null,"errorMessage":"watch callback must be a function","messagePattern":"watch callback must be a function","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/fff-bun/src/finder.ts","lineNumber":678,"sourceCode":"    options?: WatchOptions,\n  ): Result<WatchUnsubscribe>;\n  watch(\n    patternOrCallback: string | WatchBatchCallback,\n    callbackOrOptions?: WatchBatchCallback | WatchOptions,\n    maybeOptions?: WatchOptions,\n  ): Result<WatchUnsubscribe> {\n    // Overload shift: watch(cb, opts?) -> empty pattern = whole tree.\n    const noPattern = typeof patternOrCallback === \"function\";\n    const pattern = noPattern ? \"\" : patternOrCallback;\n    const callback = noPattern\n      ? patternOrCallback\n      : (callbackOrOptions as WatchBatchCallback);\n    const options = noPattern\n      ? (callbackOrOptions as WatchOptions | undefined)\n      : maybeOptions;\n\n    if (typeof callback !== \"function\") {\n      return err(\"watch callback must be a function\");\n    }\n\n    const guard = this.ensureAlive();\n    if (!guard.ok) return guard;\n\n    const trampoline = this.ensureWatchTrampoline(guard.value);\n    if (!trampoline.ok) return trampoline;\n\n    const result = ffiWatch(guard.value, pattern, options?.ignore ?? []);\n    if (!result.ok) return result;\n\n    // No startup race: the threadsafe trampoline only runs on the JS event\n    // loop, so this synchronous set always precedes the first routing lookup.\n    const watchId = result.value;\n    this.watchHandlers.set(watchId, callback);\n\n    return { ok: true, value: () => this.unwatchById(watchId) };\n  }","sourceCodeStart":660,"sourceCodeEnd":696,"githubUrl":"https://github.com/dmtrKovalenko/fff/blob/7f8537e70f0ea1210f9acbbfc4640141105cdc78/packages/fff-bun/src/finder.ts#L660-L696","documentation":"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.","triggerScenarios":"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.","commonSituations":"Refactoring from watch(cb) to watch({pattern}, cb) or vice versa and leaving arguments swapped; TypeScript-less call sites where the signature is unchecked.","solutions":["Pass the callback as the last argument: finder.watch(options, cb) or finder.watch(cb)","Verify the callback is a function before calling watch","Check the watch() overload documentation for the exact argument order you are using"],"exampleFix":"// before\nfinder.watch({ pattern: '*.rs' }); // missing callback\n// after\nfinder.watch({ pattern: '*.rs' }, (events) => { console.log(events); });","handlingStrategy":"validation","validationCode":"if (typeof cb !== 'function') throw new TypeError('watch requires a callback function');","typeGuard":"function isFn(x) { return typeof x === 'function'; }","tryCatchPattern":"const res = finder.watch(opts, cb); if (!res.ok) { /* res.error === 'watch callback must be a function' -> fix call site */ }","preventionTips":["Always pass the callback as the last argument to watch()","Enable TypeScript or JSDoc checking so swapped arguments are caught at compile time","Wrap watch setup in a helper that validates arguments once"],"tags":["validation","bun","callback"],"backgroundTag":"missing-required-argument","analyzedSha":"7f8537e70f0ea1210f9acbbfc4640141105cdc78","analyzedAt":"2026-09-10T07:26:53.407Z","contentChangedAt":"2026-09-10T07:26:53.407Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}