@preconcurrency in Swift

Hey Swifters 👋 I this guide we will explore @preconcurrency in Swift.

You have probably been loving the new Swift Concurrency features async/await, Task, actor, and the rest of the crew. But sometimes you have to interact with older code, the kind that wasn’t written with concurrency in mind. And that’s when you might stumble upon this mysterious macro @preconcurrency.

Let’s break it down. What is it? Why is it there? And when should you use it?

So. What is @preconcurrency?

@preconcurrency is Swift’s way of saying

“Hey compiler, I know this type or method wasn’t built with Swift Concurrency in mind, but I promise I’ll use it safely, treat it like it’s Sendable or safe to access across concurrency boundaries even if you can’t be sure.”

In other words, it’s a compatibility hack, a safe bridge between old synchronous code and the new concurrent world.

Now, Why Do We Need It?

Swift Concurrency introduced strict rules around what can safely be used from concurrent code.

  • Types used across threads should conform to Sendable.
  • You should avoid accessing shared mutable state from multiple tasks.
  • And the compiler checks all of this for you.

But older libraries, system frameworks, or legacy types don’t always have the right annotations. Without @preconcurrency, you will start seeing warnings or errors like:

“Type ‘XYZ’ does not conform to ‘Sendable’”
“Call to main actor-isolated instance method in a synchronous context”

That’s where @preconcurrency comes in handy.

A Real-World Example

Let’s say you are using an old DataFetcher class from a library written in Swift 5.4 (before concurrency came along).

Swift
class DataFetcher {
    func fetchData(completion: @escaping (String) -> Void) {
        // some legacy networking logic
    }
}

Now, in Swift 5.7+, if you try to use this from a Task, the compiler might complain:

Swift
let fetcher = DataFetcher()

Task {
    fetcher.fetchData { result in
        print(result)
    }
}

Even though you’re using it in an async context, Swift doesn’t know if DataFetcher is safe to use concurrently and it has no @Sendable or actor isolation. Here is how you can fix it using @preconcurrency.

Swift
@preconcurrency class DataFetcher {
    func fetchData(completion: @escaping (String) -> Void) {
        // legacy code
    }
}

Now Swift says:

“Okay, I’ll assume this class was safe before concurrency was introduced. You’re responsible for using it correctly.”

Where Can You Use @preconcurrency?

You can put it on:

  • class
  • protocol
  • extension
  • function
  • typealias
  • import statements

But Is It Safe?

That’s the catch. @preconcurrency is you telling the compiler: “Trust me.”

So it’s only safe if:

  • The type won’t be mutated from multiple tasks at once
  • The method doesn’t access shared mutable state
  • You’re not introducing race conditions

Otherwise, it’s just a shiny band-aid over a potential bug.

So use it with care. It doesn’t make old code magically thread-safe. It just silences Swift’s new warnings so you can keep moving forward.

See you in the next one 😉

More on Swift Concurrency:


Posted

in

,