libnyanpasu/clash-nyanpasu · info
Function is locked: ${fnRef.current.name}
Error message
Function is locked: ${fnRef.current.name} What it means
useLockFn wraps an async function with a lock so concurrent invocations are suppressed while one is still in flight. When a second call arrives while lockRef.current is true, the hook does not reject (the rejection path is commented out); it logs this console warning and returns undefined cast to T. The caller therefore silently gets a resolved undefined instead of the function's result.
Source
Thrown at frontend/nyanpasu/src/hooks/use-lock-fn.ts:24
) => Promise<T>
/**
* Prevents concurrent execution of async functions.
* When the function is executing, subsequent calls will be ignored until the function completes
*/
export function useLockFn<P extends any[] = any[], T = any>(
fn: LockFn<P, T>,
): LockFn<P, T> {
const lockRef = useRef(false)
const fnRef = useRef(fn)
// Update ref on each render to ensure we have the latest fn
fnRef.current = fn
return useCallback(async (...args: P): Promise<T> => {
if (lockRef.current) {
// return Promise.reject(new Error("Function is locked"));
console.warn(`Function is locked: ${fnRef.current.name}`)
return undefined as T
}
lockRef.current = true
try {
const result = await fnRef.current(...args)
return result
} finally {
lockRef.current = false
}
}, [])
}
View on GitHub (pinned to f7dbce2997)
Solutions
- Treat this as expected protective behavior — check the console for the warning to confirm a duplicate invocation was dropped, and no action is needed.
- In calling code, check the result for undefined before using it: const r = await lockedFn(); if (r === undefined) return;
- Reduce the window by making the wrapped async fn faster or disabling the trigger button/loading state while locked so users cannot re-click.
- If you need callers to know the call was dropped, uncomment the Promise.reject path (or return a sentinel) and handle it at call sites.
Example fix
// before
const onSave = useLockFn(async () => {
await save();
toast('saved');
});
// after (guard against dropped duplicate call)
const onSave = useLockFn(async () => {
await save();
return 'saved';
});
const r = await onSave();
if (r !== undefined) toast(r); // undefined means the call was locked/dropped Defensive patterns
Strategy: fallback
Try / catch
const result = await lockedFn();
if (result === undefined) {
// previous invocation still in flight; this call was dropped
return;
} Prevention
- Disable buttons / show a loading state while a locked async fn is running so users cannot re-trigger it.
- Never rely on the return value of a useLockFn-wrapped call without checking for undefined.
- Debounce high-frequency triggers (tray clicks, language switchers) before they reach the locked fn.
- Keep wrapped async work short to shrink the lock window.
When it happens
Trigger: Any rapid double invocation of a useLockFn-wrapped callback while the first async call has not finished: double-clicking a save/cancel/clear button (handleSave, handleClear, handleCancel), double-clicking the tray icon (useTrayClickHandler), rapid language switching (setLanguage), or any execute() re-entry.
Common situations: Users double-click submit buttons or spam the tray icon; slow async operations (network, IPC to the backend) keep the lock held long enough that impatient second clicks are dropped. Because the return is undefined, downstream code expecting a real result may misbehave.
Related errors
- proxy actor timed out; an operation may still be running, do
- promoted target hash mismatch
- cleanup tombstone already exists: {}
- cannot complete materialization with a target hash mismatch
- core instance retired during proxy assembly
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/8cc39b721d2e6c5b.
Report an issue: GitHub.