Part of #839 — [Audit][Round 2] A++ umbrella.
Phase: P2 · Finding: R2-8
Problem
User-facing save operations and player block mutations silently swallow errors with catch {}. The user gets no feedback that their save data wasn't deleted/renamed, and level.dat corruption is possible without surfacing. Masks real bugs (out-of-bounds, chunk-unloaded races, disk full).
Evidence
Silent save failures (user-facing)
modules/game-ui/src/screens/world_list.zig:364,384,413 (verified in audit):
self.confirmDelete(idx) catch {}; // line 364 — world delete failure silent
self.clearAllWorlds() catch {}; // line 384 — clear-all failure silent
self.renameWorld(idx) catch {}; // line 413 — rename failure silent
These touch disk. A failure means the user clicked "Delete World", got a green toast, and the world is still on disk.
Silent block mutations
modules/game-core/src/player.zig:386,405:
world.setBlock(target.x, target.y, target.z, .air) catch {}; // line 386 — break block
world.setBlock(px, py, pz, block_type) catch {}; // line 405 — place block
Minor for gameplay, but masks real bugs (chunk-unloaded races, out-of-bounds).
Logged-then-dropped save errors
modules/world-persistence/src/save_manager.zig:131-132, 167-168:
self.level_data.saveToFile(self.allocator, self.save_dir) catch |err| {
log.log.err("Failed to save level.dat: {}", .{err});
};
The save thread logs the error then drops it. level.dat corruption is possible without surfacing to the player. No failed_chunks retry-on-next-session logic visible here.
Count for context
- 27
catch {} total across the codebase; most benign (tmp-file cleanup, optional UI actions, sleep fallbacks). The 7 above are the user-facing/risky ones.
Fix
Tier 1 — surface to user (Tier 1 PR)
world_list.zig:364,384,413: replace catch {} with catch |err| { log.log.err("...", .{err}); showErrorToast("Failed to delete world"); }. Use the existing toast/notification system in engine-ui.
save_manager.zig:131-132, 167-168: introduce a SaveFailureCounter (in-memory) that's checked on next save-session startup. If non-zero, surface "N chunks failed to save last session" warning to the player on world load.
Tier 2 — propagate to logs (Tier 2 PR)
player.zig:386,405: replace catch {} with catch |err| { log.log.warn("Block mutation failed at ({}, {}, {}): {}", .{ x, y, z, err }); }. Use warn (not err) — these are expected under chunk-unload races.
- Audit the remaining 20
catch {} calls; convert any that touch disk, GPU, or user state to catch |err| log....
Tier 3 — surface to UI thread
- The save thread failure should set an atomic flag that
WorldScreen.update() polls and surfaces via a non-blocking "Save warning" indicator (similar to Minecraft's "saving..." badge).
Verification
nix develop --command zig build test
- New unit tests:
world_list_tests.zig: inject failing filesystem call, assert error toast is shown (mock the toast system)
save_manager_tests.zig: inject failing saveToFile, assert log message and counter increment
player_tests.zig: inject failing setBlock, assert warning log
grep -rE "catch \{\}" modules/game-ui/ modules/game-core/src/player.zig modules/world-persistence/src/save_manager.zig should return zero matches after fix
Constraints
- Behavior changes are limited to: previously-silent errors now produce user-visible feedback and/or log entries. No new panics.
- Tier 1 and Tier 2 can land as separate PRs.
- Conventional commits:
fix(game-ui): surface world-delete errors, fix(world-persistence): log and counter level.dat save failures, fix(game-core): log block-mutation failures.
Notes
- This is not about making every error fatal — most of these are legitimately recoverable. It's about observability: a silent disk-write failure that loses player data is a real production bug class.
- Coordinate with R2-9 (game-ui test coverage) — the toast-mock harness developed here benefits both.
Tracking: #839
Part of #839 — [Audit][Round 2] A++ umbrella.
Phase: P2 · Finding: R2-8
Problem
User-facing save operations and player block mutations silently swallow errors with
catch {}. The user gets no feedback that their save data wasn't deleted/renamed, andlevel.datcorruption is possible without surfacing. Masks real bugs (out-of-bounds, chunk-unloaded races, disk full).Evidence
Silent save failures (user-facing)
modules/game-ui/src/screens/world_list.zig:364,384,413(verified in audit):These touch disk. A failure means the user clicked "Delete World", got a green toast, and the world is still on disk.
Silent block mutations
modules/game-core/src/player.zig:386,405:Minor for gameplay, but masks real bugs (chunk-unloaded races, out-of-bounds).
Logged-then-dropped save errors
modules/world-persistence/src/save_manager.zig:131-132, 167-168:The save thread logs the error then drops it.
level.datcorruption is possible without surfacing to the player. Nofailed_chunksretry-on-next-session logic visible here.Count for context
catch {}total across the codebase; most benign (tmp-file cleanup, optional UI actions, sleep fallbacks). The 7 above are the user-facing/risky ones.Fix
Tier 1 — surface to user (Tier 1 PR)
world_list.zig:364,384,413: replacecatch {}withcatch |err| { log.log.err("...", .{err}); showErrorToast("Failed to delete world"); }. Use the existing toast/notification system inengine-ui.save_manager.zig:131-132, 167-168: introduce aSaveFailureCounter(in-memory) that's checked on next save-session startup. If non-zero, surface "N chunks failed to save last session" warning to the player on world load.Tier 2 — propagate to logs (Tier 2 PR)
player.zig:386,405: replacecatch {}withcatch |err| { log.log.warn("Block mutation failed at ({}, {}, {}): {}", .{ x, y, z, err }); }. Usewarn(noterr) — these are expected under chunk-unload races.catch {}calls; convert any that touch disk, GPU, or user state tocatch |err| log....Tier 3 — surface to UI thread
WorldScreen.update()polls and surfaces via a non-blocking "Save warning" indicator (similar to Minecraft's "saving..." badge).Verification
nix develop --command zig build testworld_list_tests.zig: inject failing filesystem call, assert error toast is shown (mock the toast system)save_manager_tests.zig: inject failingsaveToFile, assert log message and counter incrementplayer_tests.zig: inject failingsetBlock, assert warning loggrep -rE "catch \{\}" modules/game-ui/ modules/game-core/src/player.zig modules/world-persistence/src/save_manager.zigshould return zero matches after fixConstraints
fix(game-ui): surface world-delete errors,fix(world-persistence): log and counter level.dat save failures,fix(game-core): log block-mutation failures.Notes
Tracking: #839