henrygd/beszel · error

failed replacing the executable: %w

Error message

failed replacing the executable: %w

What it means

After renaming the old executable aside, update tries os.Rename(newExec, oldExec) to put the downloaded binary in place. If that rename fails, it retries via copyFile when the error is a cross-device link (EXDEV); this error is returned when the non-cross-device rename fails. tryToRevertExecChanges has already restored the original executable name, so the installation is left intact but not updated.

Source

Thrown at internal/ghupdate/ghupdate.go:215

	tryToRevertExecChanges := func() {
		if revertErr := os.Rename(renamedOldExec, oldExec); revertErr != nil {
			slog.Debug(
				"Failed to revert executable",
				slog.String("old", renamedOldExec),
				slog.String("new", oldExec),
				slog.String("error", revertErr.Error()),
			)
		}
	}

	// replace with the extracted binary
	if err := os.Rename(newExec, oldExec); err != nil {
		// If rename fails due to cross-device link, try copying instead
		if isCrossDeviceError(err) {
			if err := copyFile(newExec, oldExec); err != nil {
				tryToRevertExecChanges()
				return false, fmt.Errorf("failed replacing the executable: %w", err)
			}
		} else {
			tryToRevertExecChanges()
			return false, fmt.Errorf("failed replacing the executable: %w", err)
		}
	}

	ColorPrint(colorGray, "---")
	ColorPrint(ColorGreen, "Update completed successfully!")

	// print the release notes
	if latest.Body != "" {
		fmt.Print("\n")
		releaseNotes := strings.TrimSpace(strings.Replace(latest.Body, "> _To update the prebuilt executable you can run `./"+p.config.ArchiveExecutable+" update`._", "", 1))
		ColorPrint(colorCyan, releaseNotes)
		fmt.Print("\n")
	}

View on GitHub (pinned to b38fb7dafa)

Solutions

  1. Check that no file or directory at the executable's path conflicts (remove a stray directory with the binary's name).
  2. Re-run the update; transient locks (AV scanners) usually clear on retry.
  3. Verify no mandatory access control (SELinux/AppArmor) is denying the rename; check audit logs.
  4. Manually replace the binary: download the release, move it over the old path, and chmod +x.

Example fix

// before: a leftover directory blocks the target path
$ ls -la $(which myapp)   # myapp is a directory
// error: failed replacing the executable: file exists

// after: remove the conflicting entry and retry
$ rm -rf $(which myapp) && ./myapp update
Defensive patterns

Strategy: try-catch

Validate before calling

execPath, _ := os.Executable()
if fi, err := os.Lstat(execPath); err == nil && fi.IsDir() {
    return fmt.Errorf("%s is a directory; remove it before updating", execPath)
}

Type guard

func isCrossDeviceError(err error) bool {
    var le *os.LinkError
    return errors.As(err, &le) && errors.Is(le.Err, syscall.EXDEV)
}

Try / catch

if err := updater.Update(ctx, rel); err != nil {
    if strings.Contains(err.Error(), "failed replacing the executable") {
        log.Warn("swap failed; original binary restored, retry or replace manually", "err", err)
        return retryUpdate()
    }
    return err
}

Prevention

When it happens

Trigger: Calling Update when os.Rename(newExec, oldExec) fails with any error other than a cross-device link — e.g. the old path was recreated as a directory, the target is locked, or a permission error occurred on the target path.

Common situations: A directory named exactly like the executable exists at the target path; another process recreated/locked the executable between the two renames; SELinux/AppArmor denials blocking the rename operation; disk full preventing metadata operations in edge setups.

Related errors


AI-assisted analysis of henrygd/beszel@b38fb7dafa (2026-08-31). Data as JSON: /api/errors/4777d906de1212d5. Report an issue: GitHub.