Vinayak G Hejib
← All posts

iOS Apps That Don’t Crash: Myth or Method?

πŸ’₯

β€œAn app crash is the loudest silence your user will ever hear.” β€” Every iOS engineer, at some point (At-least I am!!).

Nothing frustrates users more than an unexpected crash. That moment when your app just… disappears. It’s not just bad UX β€” it’s a trust breaker. While some crashes are inevitable, many are preventable, especially with the power of modern Swift, improved tooling, and a proactive mindset.

In this post, we’ll walk through the types of iOS crashes, how to prevent them with Swift features, and how to detect and fix them β€” ideally before your code hits production.


βš™οΈ Why Apps Crash on iOS: Common Culprits

1. Nil Optionals (Force Unwrapping Gone Rogue)

let name: String? = nil
print(name!) // πŸ’₯ crash: Unexpectedly found nil

2. Out-of-Bounds Access

let items = ["Apple", "Banana"]
let item = items[5] // πŸ’₯ crash: Index out of range

3. Uncaught Exceptions (NSException)

Some Apple APIs (e.g., KVO, CoreData) throw Objective-C-style exceptions β€” which Swift doesn't catch.

4. Memory Issues / Retain Cycles

Retain cycles in closures or view models can cause memory bloat β†’ crashes.

5. Main Thread Violations

Doing UI updates off the main thread can lead to subtle and dangerous bugs:

DispatchQueue.global().async {
    someUILabel.text = "Oops" // πŸ’₯ boom on a bad day
}

πŸ›‘οΈ Prevention: Write Crash-Resistant Swift

1. Safe Optional Handling

if let name = getUserName() {
    print(name)
} else {
    print("Name not available")
}

Or:

print(getUserName() ?? "Guest")

2. guard For Early Exit

func showUserProfile(_ profile: User?) {
    guard let profile = profile else {
        print("Invalid profile")
        return
    }
    print(profile.name)
}

3. Use @MainActor to Protect UI Updates

@MainActor
func updateUI() {
    label.text = "Updated safely"
}

Or:

await MainActor.run {
    label.text = "Updated safely"
}

4. Weak Self in Closures

fetchData { [weak self] result in
    guard let self else { return }
    self.updateUI(with: result)
}

5. Use Value Types (Structs) Where Possible

Classes can lead to retain cycles. Structs don't. Think in value semantics when you can.


πŸ” Detecting Crashes Before They Happen

1. Xcode Runtime Sanitizers

Enable these under: Scheme > Diagnostics > Enable Undefined Behavior Sanitizer / Thread Sanitizer / Address Sanitizer

2. Unit & UI Testing with XCTExpectFailure

func testInvalidIndexAccess() {
    let array = [1, 2, 3]
    XCTExpectFailure("Array index out of bounds") {
        _ = array[10]
    }
}

3. Use Static Analyzers & Linting

  • SwiftLint: Catch unsafe force unwraps and patterns
  • SwiftFormat: Enforces code style that avoids dangerous patterns
  • Xcode Analyzer: Shift + Cmd + B = your crash prevention friend

4. Crash Monitoring in QA

Use tools like:

  • Firebase Crashlytics
  • Instabug
  • Sentry
  • BugSnag

Deploy to internal TestFlight groups or dogfood builds with crash tracking enabled.


πŸ“¦ Diagnosing Production Crashes

πŸ”Ή Use Symbolicated Stack Traces

Without dSYMs, crash reports are useless. Xcode, Bitrise, or Firebase can automate dSYM upload.

πŸ”Ή Add Custom Logs & Breadcrumbs

Crashlytics.crashlytics().log("User tapped Submit on Login")

πŸ”Ή Detect and Auto-Recover

do {
    try riskyOperation()
} catch {
    showAlert("Something went wrong. Please try again.")
}

✨ Bonus: Crash Prevention Design Patterns

Pattern What it Helps Prevent
MVVM / Clean Architecture Logic separation reduces side-effects
Dependency Injection Easier testing and mocking prevents runtime surprises
Result Type over Throw Encourages developers to handle errors explicitly
Combine / async-await Replaces callback pyramids and reduces state explosion

Real-World Pro Tips

  • Use assertion failures in dev builds to make bugs visible early.
  • Treat force-unwrapping (!) as a code smell.
  • Add crash reproducer tests for every known crash postmortem.
  • Schedule crash triage sessions for new releases.
  • Don't ignore "minor" crashes β€” they're often just the tip of the iceberg.

🌐 Bonus: Integrate Crash Analytics Proactively

Example: Firebase Crashlytics Integration

import FirebaseCrashlytics

Crashlytics.crashlytics().log("Login button tapped")
Crashlytics.crashlytics().setCustomValue(user.email, forKey: "email")
Crashlytics.crashlytics().record(error: MyAppError.someUnexpectedError)

πŸ“˜ Useful Swift Tips to Remember

Pattern Tip
! (force unwrap) Avoid unless you guarantee non-nil
try! or as! Use only in tests or tightly controlled code
assert(Thread.isMainThread) Ensure main-thread-only execution
guard let self else { return } Better in async closures than forced self!
Use struct over class Value types reduce shared state crashes

🚨 Real-World Anti-Patterns That Lead to Crashes

❌ The Overconfident Optional

let image = UIImage(named: config["iconName"]!)

❌ The Zombie Delegate

delegate?.didTapButton() // crash if delegate was deallocated

❌ Thread Roulette

DispatchQueue.global().async {
    self.tableView.reloadData()
}

πŸ”Ί Wrapping Up: Build With Crashes in Mind

Key Takeaways:

  • Modern Swift lets you write defensive, crash-resistant code.
  • Use runtime tools, sanitizers, and test coverage to catch crashes early.
  • Crash reporting is not optional in production-grade apps.
  • Your users won’t always leave reviews, but they will remember a crash.

Write code like you're the one waking up at 3AM to fix the crash report.


Your Crash Audit Checklist should look like:

  • Are all optionals safely unwrapped?
  • Are UI updates happening only on the main thread?
  • Are closures using [weak self] properly?
  • Are crash reporting tools integrated and tested?
  • Do you have test coverage for both happy and failure paths?
  • Are your crash logs symbolicated and actionable?
  • Did you simulate edge cases like low memory, network unavailability, and nil values?

If you’ve made it this far, you truly care about app’s reliability. Thanks for reading.. :)