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

  1. Treat this as expected protective behavior — check the console for the warning to confirm a duplicate invocation was dropped, and no action is needed.
  2. In calling code, check the result for undefined before using it: const r = await lockedFn(); if (r === undefined) return;
  3. Reduce the window by making the wrapped async fn faster or disabling the trigger button/loading state while locked so users cannot re-click.
  4. 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

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


AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/8cc39b721d2e6c5b. Report an issue: GitHub.