larksuite/cli · warning · ErrHeld

%w (lock: %s, syscall: %v)

Error message

%w (lock: %s, syscall: %v)

What it means

This error signals that an exclusive, non-blocking flock on a lock file failed because another process already holds the lock. It wraps the sentinel ErrHeld with %w so callers can use errors.Is(err, lockfile.ErrHeld) to distinguish 'lock is busy' from other I/O failures; the lock file path and the raw syscall error are embedded for context. It is the Unix implementation (syscall.Flock LOCK_EX|LOCK_NB) of busy-lock detection.

Source

Thrown at internal/lockfile/lock_unix.go:17

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

//go:build !windows

package lockfile

import (
	"fmt"
	"os"
	"syscall"
)

func tryLockFile(f *os.File) error {
	err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
	if err != nil {
		return fmt.Errorf("%w (lock: %s, syscall: %v)", ErrHeld, f.Name(), err)
	}
	return nil
}

func unlockFile(f *os.File) error {
	return syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
}

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Check errors.Is(err, lockfile.ErrHeld) in the caller; if held, wait and retry with backoff rather than treating it as fatal corruption.
  2. Find the competing process holding the lock (lsof on the lock file path from the message) and wait for it to finish.
  3. Do not delete the lock file while another process may hold it — flock releases automatically when the holder exits.
  4. On NFS, move the lock file to a local filesystem where flock is reliable.

Example fix

// before
cmd1: lark-cli config set a=b &
cmd2: lark-cli config set c=d   // fails: lock held

// after: serialize or retry
if err := store.Update(...); errors.Is(err, lockfile.ErrHeld) {
    time.Sleep(100 * time.Millisecond)
    return store.Update(...) // retry
}
Defensive patterns

Strategy: retry

Type guard

func IsLockHeld(err error) bool {
    return errors.Is(err, lockfile.ErrHeld)
}

Try / catch

err := lf.TryLock()
if IsLockHeld(err) {
    // busy, not corrupt: back off and retry
    time.Sleep(200 * time.Millisecond)
    err = lf.TryLock()
}
if err != nil {
    return err
}
defer lf.Unlock()

Prevention

When it happens

Trigger: tryLockFile calls syscall.Flock(fd, LOCK_EX|LOCK_NB) on the lock file and gets EWOULDBLOCK/EAGAIN (held by another process) or another flock error — the wrapper unfortunately labels both, but ErrHeld semantics target the busy case.

Common situations: Two CLI invocations racing on the same config/profile lock (e.g. concurrent `lark-cli auth login` in two terminals); a stale lock held by a crashed process whose fd lingers (rare for flock — dies with the process); NFS mounts where flock semantics are unreliable; a long-running command (update/install) still holding the lock.

Related errors


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/7b727e7cf3abfb27. Report an issue: GitHub.