All articles
iOS9 min read

When Swift Code Goes Rogue: Race Conditions vs. Deadlocks Explained 🧨

Ever seen your Swift app crash without mercy? 🧨 Meet the chaos duo: Race Condition (the reckless sprinter) and Deadlock (the stubborn…

K
Karan Pal
Author
Image generated by AI
Image generated by AI

🧠 When Swift Code Plays Villain

Have you ever stared at your screen wondering “How the heck did that happen?” — your app was working fine yesterday, but now it’s crashing harder than your hopes during WWDC seat reservations. 😵‍💫

Welcome to the world of concurrency gone wrong — where your beautifully written Swift code can suddenly turn rogue. Not because it wants to… but because somewhere in the chaos of multithreading, you let two monsters in the room:

These aren’t just obscure bugs you read about in Stack Overflow nightmares — they’re the kind that show up in real production builds, sometimes months after your last code push, just to ruin your weekend 🍕.

In this article, we’ll unpack these two beasts, see how they wreak havoc in Swift, and more importantly, how to outsmart them before they outsmart you.

Let’s start with our first troublemaker: the race condition.

🏃‍♂️ Race Condition: The Reckless Sprinter

Imagine this: Two chefs walk into a kitchen and both reach for the same frying pan — at the exact same moment. One tries to melt butter, the other dumps pancake batter. Meanwhile, a third chef waltzes in and throws maple syrup on the stove instead of the food.🔥

The result? Not breakfast. Just chaos.

That, my friend, is a race condition in action: multiple cooks (threads) messing with shared ingredients (data) without coordination — leading to unpredictable, often hilarious, but mostly horrifying results.

🚨 What is a Race Condition?

A race condition happens when multiple threads access shared data concurrently, and at least one thread modifies it, leading to unpredictable results. It’s like a competition where nobody knows the rules — and someone always loses (spoiler: it’s your app).

In Swift, this usually creeps in when:

😵‍💫 A Quick Example in Swift

var balance = 0

DispatchQueue.global().async {
    for _ in 1...1000 {
        balance += 1
    }
}

DispatchQueue.global().async {
    for _ in 1...1000 {
        balance -= 1
    }
}

Expected result? Probably 0.

Actual result? Who knows! 🌀

Since balance isn’t thread-safe, the += 1 and -= 1 operations collide, and memory gets updated in a corrupted way.

🧯 How to Prevent It

Swift gives you several tools to tame this chaos:

actor BankAccount {
    private var balance = 0

    func deposit(amount: Int) {
        balance += amount
    }

    func getBalance() -> Int {
        return balance
    }
}

Goodbye, rogue threads. Hello, thread-safe sanity.

🔒 Deadlock: The Eternal Standoff

Picture this: Two people walk into a narrow hallway from opposite ends.

Each politely holds the door open for the other.

“After you.”

“No, I insist. After you.”

They smile, they nod, they wait.

And wait.

Cut to three hours later — same hallway, same smiles, same standoff. 😐

That’s a deadlock in a nutshell: two (or more) threads, each holding a resource the other needs, and neither willing to let go. Your app doesn’t crash. It doesn’t move either. It just… exists. Like a zombie that really needs to be rebooted.

It’s not a bug that screams — it’s one that stares at you silently while draining your battery.

🧩 What is a Deadlock?

A deadlock occurs when two (or more) threads:

  1. Hold a resource (like a lock),
  2. Request another resource that’s already locked,
  3. Wait forever for each other to release it.

The result? Your app hangs in awkward silence until the user rage-quits or your health check pings you at 3 AM.

💀 The Classic Swift Example

let queue = DispatchQueue(label: "com.serial.queue")

queue.sync {
    queue.sync {
        print("This will never be printed")
    }
}

This is a self-inflicted disaster:

😬 Real-World Deadlock Situations

🛡️ How to Avoid Deadlocks

Here’s a fixed version of the earlier example using async:

queue.async {
    queue.async {
        print("This *will* be printed")
    }
}

Now that we’ve met the twin troublemakers of concurrency, let’s compare them side by side — because knowing which monster you’re dealing with is half the battle. ⚔️

⚔️ Race Condition vs. Deadlock: Know Thy Enemies

Let’s break it down like a developer showdown. Who’s messier? Who’s sneakier? Let’s see:

🏃‍♂️ Race Condition

🔒 Deadlock

Simple rule of thumb?

👉 If your data is wrong, blame a race condition.

👉 If your app is frozen, deadlock’s probably laughing somewhere in your codebase 😈.

🛠️ How to Catch Race Conditions and Deadlocks in Swift

Swift doesn’t just throw these bugs in your face. Nooo. They sneak in, throw a bug party, and leave without saying goodbye. But with the right tools and habits, you can catch them red-handed.

🧪 1. Turn on the Thread Sanitizer (Xcode)

Your best friend when dealing with race conditions.

Run your app, and if Swift spots a race, it’ll call it out like a schoolteacher with a red pen. You’ll get a detailed thread trace showing who accessed what and when.

_Note:_ It slows things down but is a must-have during testing and CI.

🧰 2. Use Logging and Breakpoints

Add logs when accessing shared variables or using locks. It helps in:

💡 Bonus tip: Add Thread.current in your logs to know which queue is doing what.

🧵 3. Use Structured Concurrency Where Possible

Actors, Tasks, and async/await aren’t just for fun — they automatically protect shared state and reduce thread mishaps.

actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }
}

Much cleaner than juggling locks and hoping for the best.

⛓️ 4. Avoid Sync-on-Sync Crimes

Never, ever call sync inside another sync — especially on the same serial queue. That’s a guaranteed ticket to deadlock hell 😈.

🔄 5. Test Under Pressure

Race conditions love chaos.

Sometimes, you need to poke the bug before it bites.

🧭 6. Instruments: Time Profiler + Threads

When your app freezes (possible deadlock), fire up Instruments:

✨ Bonus: Keep It Simple

Sometimes, the best way to avoid race conditions and deadlocks is… don’t over-complicate it. Don’t create queues you don’t need. Don’t lock what you don’t access across threads. Keep concurrency lean and predictable.

🧹 Conclusion: Don’t Just Write Swift Code — Tame It 🧘‍♂️

Concurrency is like fire: 🔥

Used wisely, it cooks your app to perfection.

Used carelessly, it burns everything down — including your user’s trust and your debugging sanity.

Now you know the difference:

Both are dangerous. Both are fixable.

And both love hiding in places where you least expect them. 👀

So the next time your app misbehaves, don’t just shout “Xcode is broken!”

Put on your debugging cape, enable Thread Sanitizer, and go full Sherlock mode 🕵️‍♂️.

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading