Skip to content
Development

A segfault that wasn't a bug, and the API I finally deleted

By Victor Da Luz
iosswifttestingdev-logdeep-cut-atlas

This app was later renamed Deep Cut Atlas. It’s called “Discoverer” throughout below, because that’s what it was called on the day this happened.

This was supposed to be the easy cleanup issue. A repo review had flagged a handful of things in Discoverer’s MusicKit service layer: a few methods nobody called anymore, two error types that showed ugly default messages instead of real copy, some duplicated matching logic, and a couple of playlist lookups doing more work than they needed to. None of it was a functional bug. All of it was the kind of small debt that just needed someone to actually go delete it.

The dead code was oddly satisfying to remove. Four methods, zero callers, confirmed by grep before I touched anything. One parameter that was always passed the same hardcoded value at every call site, which meant an entire branch of code behind it had never run outside of the one place it was actually needed. Deleting code that provably does nothing is one of the few refactors that feels completely safe, because you’re not guessing about behavior, you’re just removing a path nothing takes.

Then I ran the test suite and every single test failed instantly. Not “the tests I touched.” All of them, unrelated ones included - string helpers, array chunking utilities, things that had nothing to do with my change. Each one reported a runtime of zero seconds, which is the tell that something crashed the whole test process rather than any individual test actually failing.

The crash log pointed at a real memory access violation inside a completely different function than the one I’d been editing, and the calling function it named doesn’t actually call that function anywhere in the current code. That mismatch - a named caller that couldn’t possibly call the named callee - is what made me suspicious instead of just trying to fix a phantom bug. Removing methods from a shared interface changes the underlying memory layout of how those calls get dispatched, and an incremental build can end up gluing together compiled pieces from before and after the change. The result runs, but it jumps to the wrong place while the debugger still shows you the old labels.

A full clean and rebuild fixed it instantly, no code changes. That’s the whole trick: when you’ve just changed what methods exist on something, and the very next test run segfaults in a way that makes no sense, don’t start debugging the named function. Clean first.

The rest of the cleanup landed the way cleanups should: quietly. Two error types now say something a person would actually understand instead of a raw enum case number. A duplicated string-matching block became one shared, tested helper. Two playlist lookups that used to fetch every playlist and scan for the one they wanted now ask for that one directly. None of it changes what the app does. All of it makes the next person’s job smaller.

Related reading