Skip to content

UpdateChecker.saveCache silently swallows all write errors #336

@bguidolim

Description

@bguidolim

Summary

UpdateChecker.saveCache(_:) catches every error from the cache-write pipeline (directory creation, JSON encoding, file write) and drops them on the floor:

// Sources/mcs/Core/UpdateChecker.swift (in saveCache)
} catch {
    // Cache write failure is non-fatal — next check will just redo network calls
}

This was surfaced during #334 review. Filing separately since it's pre-existing and outside the #334 fix scope.

Why this matters

The inline justification ("non-fatal — next check will just redo network calls") is only partially true:

  1. Permission/filesystem errors repeat forever. If createDirectory fails because ~/.mcs/ is read-only (recovery mode, disk full, bad ACL, sudo mcs aftermath), every session silently re-runs full network probes. No feedback to the user, no degraded-mode signal.
  2. Encoding errors are programmer bugs, not transient. JSONEncoder failing on CachedResult means the type is broken — will never self-correct.
  3. Silent-failure compounds with other silent paths. When invalidateCache later can't delete a file saveCache silently failed to write, the two failures chain with zero diagnostic trail. The PR author had to add a fileExists post-check in seedUpdateCheckCache test helper specifically to guard against this defense-in-depth hole.

CLAUDE.md's convention is explicit: "Never use try? to silently discard errors — use do/catch and surface the error (via output.warn(), logging, or propagation)." The same reasoning applies to empty catch {} blocks.

Proposed fix

Match the pattern used by invalidateCache after #334: return a Bool (or throw), and have the single caller (performCheck) decide how to surface the failure. Or simpler: accept a CLIOutput? parameter and warn at the source, preserving the underlying error in the message.

@discardableResult  // ← only if truly safe to ignore, which it isn't
func saveCache(_ result: CheckResult) -> Bool {
    // ... existing encode + write logic, with catch returning false
}

Callers that can't reasonably react (e.g., the SessionStart hook path) can _ = checker.saveCache(result) with an explicit comment — forces the decision to be auditable rather than implicit.

Scope

  • Sources/mcs/Core/UpdateChecker.swiftsaveCache(_:)
  • Call sites: performCheck, --hook path
  • Tests: UpdateCheckerCacheTests — add a permission-error case (chmod'd tmp dir)

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions