I keep coming back to Swift's actor model, and something small keeps nagging at me. I say this as someone who loves the language (I have written before about why Swift is my favorite language I never got paid to use, and I predicted its rise back in 2014). The whole promise of actors was isolation: give each unit of concurrency its own protected state so the compiler can rule out data races. That goal is exactly right, and I think it was absolutely needed. Yet as an outsider looking in, I keep noticing that an actor still feels like a class that is a little different for thread safety, while Rust reaches stronger, more versatile isolation with no actor construct at all. The question that pulls me in is simple: if Rust needs no actors to be safe across threads, what is Swift's actor model actually buying, and why does it feel like it stops one step short?
Two Kinds of Thread Safety
The first thing worth separating is that "thread safety" is really two different properties, and Swift and Rust attack them at different layers. The first property is data-race freedom (no two threads touch the same mutable memory at once without synchronization). The second is isolation (a unit of concurrency owns its state, and nothing else can reach into it).
Rust guarantees data-race freedom with essentially no runtime concept of an actor. Swift's actors are a mechanism aimed at isolation, and they get data-race freedom as a consequence. That asymmetry is the root of almost everything that feels off to me. Rust weaves safety into the type system uniformly, so every type carries its own thread-safety story. Swift attaches safety to a reference type (the actor) that lives inside a language rich with shared, mutable class references. Both are reaching for the same summit; they just start their climb from very different base camps.
Why Rust Reaches Safety Without Actors
Rust's data-race freedom falls out of ownership and borrowing plus two marker traits, all checked at compile time. The starting point is that access permissions are encoded directly in the types. For some type T, you work with three forms: T is an owned value, &mut T is an exclusive (unique) mutable borrow, and &T is a shared, read-only borrow. Ownership means T is yours; &mut T means you have the only handle that can mutate it right now; &T means you are one of possibly many readers.
The foundation is a single rule the borrow checker enforces on those forms: at any moment you can have many shared references (&T) or exactly one mutable reference (&mut T), never both, and never two &mut T at once. The Rust Book spells this out in its chapter on references and borrowing. A data race requires aliasing and mutation and concurrency together, so by statically forbidding the aliasing-plus-mutation combination, Rust makes the race impossible before threads even enter the picture. Two more guarantees round this out: every reference carries a lifetime and cannot outlive the T it points to (so you can never hold a dangling reference), and each owned value is freed exactly once when its single owner goes out of scope (so there is no double-free). The effect is that you must prove to the compiler you have no data races before your program will build.
A direct consequence is that Rust will not let you mutate a value through a shared &T at all. If you want to share something across threads and still change it, you have to reach for an explicit synchronization type, and the type system will insist on it.
A Mutex<T> hands out access to one writer at a time; a RwLock<T> models the multiple-readers, single-writer pattern, granting many concurrent read guards (which behave like &T) or a single exclusive write guard (which behaves like &mut T), but never both at once. These types move the "shared XOR mutable" check to runtime while preserving exactly the same invariant the borrow checker enforces at compile time, and because they are Sync the compiler can then let the wrapped value be shared safely. The rule never bends; you simply choose where it is enforced.
On top of that, Rust adds two auto-derived, compositional traits. Send means a type can be moved to another thread, and Sync means a shared reference to it can be used from multiple threads. These propagate automatically through every type: Rc<T> is not Send because its reference count is non-atomic, while Arc<T> is Send because its count is atomic. The Rustonomicon chapter on Send and Sync walks through exactly how the compiler derives and checks this. When you hand a value to another thread through a channel, ownership transfers and the sender can no longer touch it.
The Rust community has a name for the result of all this working together: fearless concurrency. Because the ownership and borrowing rules that already keep single-threaded code memory-safe are the very same rules that rule out data races, adding threads does not require a new mental model, and a large class of concurrency bugs becomes a compile error rather than a rare production crash.
That transfer point is where I have to correct my own intuition. I used to describe Rust's safety in terms of copies, as if independence came from duplicating data. The mechanism that actually makes Rust both safe and cheap is a move that invalidates the source, not a deep copy. Nothing is duplicated; ownership simply relocates, so there is no performance penalty and no size limit. My worry about "structs too big to copy" mostly dissolves once I think in moves instead of copies. So Rust gives you per-unit isolation exactly when you want it (channels, Mutex<T>, thread-local ownership) without ever imposing an actor runtime. Actors in Rust exist only as an optional library pattern, such as Actix, because the language already provides the guarantee that actors elsewhere provide at runtime.
The Monumental Task Swift Took On
Here is where I want to be fair, because it is easy to critique a result without honoring the pursuit. Swift set out to add strict, safe concurrency to a language that was already mature, already shipping in billions of devices, and already committed to seamless interoperability with C and C++, reference-semantics classes, automatic reference counting, and a stable ABI. Retrofitting compile-time data-race safety onto that foundation, and doing it progressively so existing code keeps working, is one of the more ambitious language-evolution efforts I have watched unfold. The Swift 6 migration path lets teams opt into complete concurrency checking incrementally rather than through a single breaking cutover, which is a remarkable amount of care for real-world codebases.
That care is exactly what shapes the design, and it is where I would gently push back on my own earlier framing. It is tempting to say the language "insists" on shared references or "will not give up" C compatibility, but that anthropomorphizes a set of deliberate engineering priorities. The Swift team chose to preserve reference semantics, ARC, and C and C++ interoperability because those choices serve the platform's users, and safety then had to be built on top of that substrate rather than baked into its core. Given that starting point, Sendable has to exist as a separate, viral constraint (defined in SE-0302), because an actor is a reference type whose guarantee is only that access to its mutable stored properties is serialized through its executor. The actor boundary is only as strong as the Sendable-ness of what crosses it, which is why the actor reads to me as a serialization point rather than a fully sealed environment.
The pieces that make this work are genuinely clever, not incidental complexity. Region-based isolation (SE-0414) lets the compiler prove that a non-Sendable value is disconnected from every other region, so it can be transferred into an actor safely without full ownership tracking, and sending parameters (SE-0430) build directly on that analysis. Swift is even importing ownership ideas the language did not start with, through noncopyable types (~Copyable, SE-0390) and the borrowing and consuming modifiers (SE-0377). These features land in the middle of an existing model, so they feel additive rather than foundational, but each one is a considered step toward the same safety Rust gets from its base.
It is worth noticing that Swift's own concurrency proposals describe an actor as an "island of single-threadedness" with a mailbox as its synchronization point, echoing the classic actor model. Systems like Erlang get strong isolation because messages are copied or immutable, so no shared references escape. Swift kept the serial-executor mailbox but not the isolation-by-value, which is a reasonable call on a platform like iOS where cheap process-per-actor isolation is not available and the whole app is a single sandbox.
The Trade-off, Named
So what is the trade-off, stated plainly? Swift chose ecosystem continuity (existing code, C and C++ interop, ARC, ABI stability) and is buying back compile-time concurrency safety on top of it through Sendable, region analysis, and move-only types. Rust chose ownership as its foundation and gets data-race freedom uniformly and for free, at the cost of a steeper up-front model and no seamless C++ object interop of Swift's kind. Neither choice is misguided; they optimize for different things. Swift's actor model delivers serialized access and a strong, best-effort isolation boundary, and Swift 6 genuinely catches data races, which is a real and hard-won result. What it does not (yet) offer is Rust's uniform, type-level guarantee, because that would require ownership at the core rather than as an opt-in layer.
The interesting part is that the opt-in layer already exists in pieces. Noncopyable types, consuming and borrowing ownership, @Sendable closures, and complete strict-concurrency checking together add up to something close to a stricter dialect of Swift, just delivered as gradual, composable constraints instead of a separate mode. That progressive path is the whole point: rather than a single dramatic break, Swift is tightening the guarantees one proposal at a time, letting an enormous body of existing code move forward at its own pace. Rust reached safety by designing for it from day one; Swift is reaching for the same safety while carrying a decade of compatibility on its back, and it is getting there in careful, deliberate steps.
Rust does not need actors because ownership, Send, and Sync, plus move-by-default, make data races a compile error at the type level, uniformly. Swift's actors serialize access on top of a shared, C-compatible core, so full isolation has to be rebuilt progressively with Sendable, regions, and move-only types. The more I sit with it, the less this looks like a shortcoming and the more it looks like the tax of backward compatibility being paid down honestly, one release at a time. It rhymes with what I found in My Swift Journey and in Language Choice in the LLM Era: Swift keeps making sound engineering choices under real constraints, and the pursuit of strict, safe concurrency on a mature language is worth admiring even where it has not yet caught up to a language that started from ownership. The trade-off is not that Swift got it wrong; it is that it chose to bring everyone along, and that is a harder road.
Sources
| Source | Link |
|---|---|
actor — Apple Developer Documentation | developer.apple.com |
Sendable — Apple Developer Documentation | developer.apple.com |
| SE-0306: Actors | swift-evolution |
SE-0302: Sendable and @Sendable closures | swift-evolution |
| SE-0414: Region-based isolation | swift-evolution |
SE-0430: sending parameters and results | swift-evolution |
SE-0390: Noncopyable structs and enums (~Copyable) | swift-evolution |
SE-0377: borrowing and consuming parameter ownership modifiers | swift-evolution |
| Swift 6 Migration Guide (data-race safety and strict concurrency) | swift.org |
| Automatic Reference Counting — The Swift Programming Language | docs.swift.org |
| Mixing Swift and C++ — Swift.org | swift.org |
| What Is Ownership? — The Rust Programming Language | doc.rust-lang.org |
| References and Borrowing — The Rust Programming Language | doc.rust-lang.org |
| Message Passing / channels — The Rust Programming Language | doc.rust-lang.org |
| Fearless Concurrency — The Rust Programming Language | doc.rust-lang.org |
| Validating References with Lifetimes — The Rust Programming Language | doc.rust-lang.org |
Running Code on Cleanup with the Drop Trait — The Rust Programming Language | doc.rust-lang.org |
std::marker::Send — Rust standard library | doc.rust-lang.org |
std::marker::Sync — Rust standard library | doc.rust-lang.org |
std::sync::Mutex — Rust standard library | doc.rust-lang.org |
std::sync::RwLock — Rust standard library | doc.rust-lang.org |
| Send and Sync — The Rustonomicon | doc.rust-lang.org |
| The Rust Programming Language (official site) | rust-lang.org |
| Actix (actor framework for Rust) | actix.rs |
| Actor model — Wikipedia | wikipedia.org |
| Erlang (official site) | erlang.org |