try? turned an outage into "all caught up"
Right after adding error injection to my mocks, I went looking at the one place the new seam couldn’t reach. It turned out to be hiding the worst bug in the app.
The problem
Deep Cut Atlas builds its feed by fetching every library artist’s catalog, a few artists at a time:
let tasks = chunk.map { artist in
Task { () -> [LibraryAlbum] in
(try? await self.service.fetchCatalogReleases(for: artist)) ?? []
}
}
That try? ... ?? [] looked harmless. One flaky artist shouldn’t kill a 200-artist sweep, right? Per-artist resilience. I even had a comment explaining the chunking was a rate-limit guard.
But follow the empty array downstream. The diff treats it as “this artist has no releases”. The result gets cached with a 24-hour TTL. So if the network dies mid-refresh: every artist “has no releases”, the diff is empty, the empty feed is cached over the good one, and the UI cheerfully reports “You’re all caught up!” for the next day.
An outage didn’t look like an outage. It looked like success with no data - which is exactly what try? does: it makes failure indistinguishable from absence.
The fix
Each fetch now returns a Result instead of swallowing, failures get logged per artist, and the sweep reports whether it was complete:
private struct CatalogSweep {
let albums: [LibraryAlbum]
let isComplete: Bool
}
Three behaviors fall out of it. If all artists failed, throw - the existing error path handles it: a cold load shows the failure, a background refresh keeps showing the cached feed. If some failed, show the partial feed (fresh-but-partial beats nothing) but don’t cache it - the on-screen feed self-corrects on the next refresh, while a cached one would be the baseline for a day. If none failed, cache as before.
The mock needed one more seam to test this: per-artist failure injection (failingArtistNames: Set<String>), because the method-level injection from the previous round could only fail all artists at once. Four tests pin the matrix: total and partial failure, on cold load and on background refresh.
Lessons
try? in a data pipeline means failure becomes data. If that data feeds a cache, you’ve built a mechanism for persisting outages as facts.
Resilience and visibility are separate decisions. Keeping the sweep alive past one flaky artist is right; pretending the artist had nothing is not. Result gives you both.
And writing failure tests is how I found this. The error-injection seam didn’t just cover existing branches - it made me notice the one error that never reached a branch at all.
Related reading
I only fixed the screenshot I was asked about, not the ones that were also broken
One recaptured marketing shot looked done - until the deflating question: is this really every screenshot? The other three in the same set were stale too, each in a different way.
A tracklist section, and why it took 30 minutes
One protocol method, a reused state enum, a routing convention that held, and a lint budget that forced a split worth making anyway.
The App Store screenshots nobody updated for two weeks
Two folders that look interchangeable and aren't, a JSON cache that survives reinstall, and three rounds of 'looks done' that weren't.