The logs were clean.

The feature mostly worked.

So we did what engineers do best.

We started reading code.

πŸ“œ The Investigation Begins

The file was massive.

  • Helpers
  • Extensions
  • Business rules
  • Edge-case handling
  • Defensive checks everywhere

Over 1,000 lines of code.

We zoomed into the complex parts first β€”

surely the bug lived where the logic was hard.

It didn't.

πŸ˜… The Bug That Humbled Everyone

Here were the first few lines:

class PaymentManager {
    var delegate: PaymentDelegate?
    
    init(delegate: PaymentDelegate) {
        self.delegate = delegate
    }
}

Looks harmless.

No crashes.

No warnings.

No compiler complaints.

Until you notice what's missing.

πŸ’₯ The Real Problem

The delegate wasn't weak.

weak var delegate: PaymentDelegate?

That single keyword.

Missing.

As a result:

  • PaymentManager retained the delegate
  • The delegate retained PaymentManager
  • ARC couldn't break the cycle

πŸ” Memory leak. Silent. Persistent. Deadly.

🀯 Why This Hurt So Much

Because everything else was… beautiful.

  • Perfect abstractions
  • Clean architecture
  • Defensive coding
  • Edge cases handled

But all of that effort couldn't save us from line 3.

🧠 The Psychological Trap

Here's what really happened:

🧠 "The bug must be in the complex logic."

🧠 "Senior code doesn't fail in simple places."

🧠 "Let's skip the boilerplate."

And that bias cost us hours.

Complexity hides bugs β€” but simplicity hides them better.

πŸ§ͺ How We Found It (Eventually)

Not by reading deeper.

But by stepping back.

  • Memory Graph Debugger
  • Watching objects refuse to deallocate
  • deinit never firing
deinit {
    print("PaymentManager deallocated")
}

Silence.

That silence pointed us to the top.

⚠️ The Real Lesson

Most bugs aren't:

  • In algorithms
  • In edge cases
  • In clever abstractions

They live in:

  • Initialization
  • Ownership
  • Lifecycles
  • Defaults

Places we stop paying attention to.

πŸ› οΈ My New Review Rules

After this incident, I changed how I review code:

βœ… Start from the top

βœ… Scan ownership first

βœ… Look for weak, unowned, lifetimes

βœ… Verify deinit

βœ… Never skip "boring" lines

🎯 Final Thought

We love complex bugs.

They make us feel smart.

But real-world bugs are boring.

And that's what makes them dangerous.

Next time you review code β€”

don't rush to line 500.

Start at line 3.