How A Simple, Stupid Error Keeps Crashing FocusPasta
A Debugging Tale of Crashes, Clues, and Careless Mistakes
Let’s start with the good news.
FocusPasta, my pasta-themed Pomodoro app, has been live on the App Store for three weeks! Since then, it has received 8.68K impressions, 199 downloads, and even three paying users (including myself, of course).
Now, on to the bad news.
In those same three weeks, there had been 15(!!!) reported crashes in FocusPasta. And that’s only from users who opted to share their usage data with developers, so the actual number is probably much higher!
How is this even possible?
Here’s the real kicker: I had no idea why this was happening.
I find it incredibly weird that FocusPasta keeps crashing. For one, we don’t force-unwrap optionals or use fatalError() in this house — we handle our errors gracefully! For two, I’ve been using FocusPasta for a few months now, along with a few beta testers on TestFlight, and FocusPasta has NEVER crashed… except that one time when a user triggered a broken IBAction by clicking on an invisible button I’d forgotten to remove (because, well, it’s invisible 🤡).
It must be out of my pay-grade.
I was really worried at this point.
I trusted my code and believed I’ve covered all edge cases, but as a new app developer, my Swift knowledge was still surface-level and I have not ventured into the world of instrumentation and performance tuning. So when crashes started happening, I was pretty sure they were caused by issues way beyond my expertise — things like memory leaks, thread safety, race conditions, deadlocks, and all that scary stuff.
Because of this, I kept pushing off the debugging. The first crash report puzzled me, but I shrugged it off. Then there were three the next day. Seven soon after. By the time it hit fifteen, I knew something was seriously wrong, and I could no longer ignore it.
The Investigation Begins
Step 1: The Crash Logs
First up, I pulled up the Crash Organizer in Xcode, which provides information about app crashes collected from users.
To my relief, there was only one crash log listed, with “15 devices” associated with it. This meant that all the crashes had the same root cause, so I only had one issue to investigate!
Then, I saw code. Familiar function names! I breathed another sigh of relief — this meant that the cause of the crash was my own damn fault, not some deep, mysterious performance issue. It was something actually within my control, something I might even know how to fix.
Let me walk you through the app’s flow so you can better understand the exception trace above. In FocusPasta, users can start a timer to help them stay focused. When the focus session ends, they are taken to PastaRainBreakViewController
, where a 5-minute timer begins. A notification is also scheduled at this point to alert users when their break time has ended.
According to the crash log above, the issue occurs during the initialization of this notification.
Step 2: Apple Developer Forums to the Rescue
Next, I googled “UNTimeIntervalNotificationTrigger crash”, and got pretty lucky — the first result was a thread from Apple Developer Forums that had exactly what I needed:
According to the thread, a crash will happen when the notification is scheduled with a timeInterval
of 0
. This led me to suspect that I might be passing a 0-second interval to my notification scheduler, and I set out to verify this hypothesis.
Tracking Down the Culprit
Suspect 1: The Initial Setup
Since this crash wasn’t occurring on my phone or my testers’ devices, I suspected an issue during the initial setup — something only new users would go through. Since this flow is skipped on our devices, it would explain why existing users did not encounter the bug.
It Works On My Machine
To replicate the conditions of users experiencing the crash, I set up a fresh simulator device on my Mac and installed FocusPasta. This provided me with a clean slate to observe the initialization process, including the configuration of default settings.
I checked the logs for this initialization:
Everything looks good! The logs clearly showed that the settings were set to the correct default values in the User Defaults.
Suspect 2: The Notification Manager
With the setup code off the hook, the next suspect was the notification manager. The breakDuration
had been configured correctly, so perhaps an issue was causing it to be read as 0
instead of its actual value from User Defaults.
I investigated the code responsible for scheduling notifications during the break, which runs when the user enters the view controller. At this point, the breakDuration
is fetched from User Defaults, which — according to the logs in Exhibit A — was correctly set to 5 minutes (300 seconds).
So why was the notification scheduler receiving 0
seconds instead of the expected 300
? A parsing issue? A type mismatch?
Curious, I added some logs to check the value of breakDuration at runtime. I launched the app, started a focus session, waited for the focus timer to finish, and clicked on “take a break”.
Looks like we’ve got our guy.
Lo and behold — the break view controller popped up, appeared for a split second, and then immediately dismissed itself. No pasta rain, no 5-minute countdown timer.
For a moment, I felt a surge of relief. Finally, I found the culprit!
But that relief was short-lived. Piece by piece, the realization settled in — and with it, a growing sense of dread. My heart sank. This was so much worse than I had thought.
This wasn’t just a notification issue — it was a break duration problem. Somehow, the default break duration had been set to 0 minutes. This meant that every single new FocusPasta user had been affected: the break view dismissing itself the moment it appeared. And for those with notifications enabled, it was outright crashing the app.
What I thought was a minor notification bug had turned out to be a critical issue that broke one of the app’s core features.
Uncovering the Motive
Now that I knew what was happening, the next question was why. Somewhere in my code, something had gone wrong. How did the default break duration end up as zero when the logs clearly said otherwise?
Let’s review the facts:
When the break view controller is initialized, it pulls the value of
breakDuration
from User Defaults.On first launch, we set the default value of
breakDuration
in UserDefaults to 5.The
breakDuration
is set as an Integer, and is retrieved as an Integer, no parsing or type conversion involved.Yes, I am using the correct key to store and fetch
breakDuration
.Yes, I am reading from the same User Defaults instance.
Seriously, what is the problem here? Just take a look for yourself!
Aaaaand There It Was
And then I see it — right in my face.
Did you catch it? Go back to Exhibit B and take a closer look.
The truth had been hiding in plain sight all along. I was merely logging the fact that I was setting values in User Defaults, but I wasn’t actually setting them! It was all talk and no action — yep, that’s me alright.
I checked the commit history, and there it was: a bug I had introduced in back in November, when I refactored my print logs into proper loggers.
Case Closed.
With the culprit and motive identified, everything clicked into place. If the default value for breakDuration
was never properly set, then any new users will get the default fallback value: zero.
This explains everything:
The Instant Dismissal: When the break view controller pulls the
breakDuration
value, it gets0
. The timer starts and finishes instantaneously, causing the view controller to dismiss itself right after being presented.The Notification Crash: When the notification scheduler is given a
timeInterval
of0
, it triggers the crash I uncovered earlier from the Apple Developer Forums.It Works On My Machine: This issue only impacts new users who installed the app after November 2024. For me and my beta testers,
breakDuration
was already set, so we were never affected.
The Clue Was Right There
In hindsight, I would have caught this bug earlier if I had been more attentive.
While reviewing FocusPasta’s analytics on Firebase, I noticed something strange: every user, except for two (including me), had always completed the full duration of their breaks instead of ending them early.
As someone who constantly skips or cuts my breaks short, I found it oddly amusing.
Lessons Learnt
I’m happy to report that this bug has since been patched, and most users have updated their apps to the new version.
However, this experience has taught me a hard lesson: I need to be extra careful when pushing changes to the App Store, since there’s no guarantee that users will update to the new version! Every release needs to be handled with extra care.
Here are my key takeaways:
Test All Critical Features: Before releasing a new version, I should thoroughly test all app features, especially the critical ones that form the main flow of the app. Not just on my own device.
Pay Attention To Analytics: Sometimes, what may seem like an interesting anomaly could actually be a sign of a deeper issue. I should always approach data with a problem-solving mindset, instead of dismissing unusual patterns as “just the way people are”.
Review My Own Code Changes: As a solo developer, it’s easy to fall into the habit of pushing changes directly to the main branch. Unfortunately, this makes it easy for mistakes to slip through. From now on, I’ll take the extra step of reviewing my changes before merging, especially now that real users are using the app.
All in all, I’m pretty disappointed that my initial users didn’t get to experience FocusPasta the way I intended, but I’m grateful that I managed to catch the issue (relatively) early and fix it quickly.
This was a tough lesson, but one that made me a better developer. Next time, I’ll catch the bug before it catches me!