The tool shed dilemma
You are building a high-performance image processing library. You want it to run on Linux servers, Windows desktops, embedded cameras, and WebAssembly in the browser. You also refuse to ship code that segfaults or leaks memory. You look at Swift. Swift is fast, safe, and has excellent tooling. But Swift relies on a runtime and Automatic Reference Counting that adds overhead and creates dependency headaches outside Apple's ecosystem. You look at Rust. Rust compiles to bare metal, runs anywhere, and promises safety without a runtime. The choice isn't just about syntax. It is about where your code lives, how it manages memory, and what the compiler demands from you before it lets you ship.
Memory management: Leases versus guest books
Rust and Swift both eliminate garbage collection, but they take opposite paths to safety. Rust uses ownership and borrowing. Swift uses Automatic Reference Counting, or ARC.
Rust treats memory like a lease. Every value has exactly one owner. The owner is responsible for cleaning the value up. You can lend the value to others, but the rules are strict. You can have many readers, or exactly one writer. The compiler checks these rules at compile time. If you break them, the code does not compile. There is no runtime cost for these checks. The safety is baked into the binary.
Swift treats memory like a shared apartment with a guest book. Every object has a counter. When you pass a reference to the object, the counter goes up. When a reference goes out of scope, the counter goes down. When the counter hits zero, the object is destroyed. This happens at runtime. The safety is enforced by the CPU updating counters. This makes Swift easier to write because you don't have to think about who owns what. You just pass references around. But every reference copy costs a CPU cycle to update the counter. And if two objects hold strong references to each other, the counter never hits zero. You get a memory leak.
Rust prevents memory leaks by design. The ownership graph cannot have cycles. If you try to create a cycle, the compiler rejects it. Swift allows cycles. You must manually break them using weak or unowned references. If you forget, your app leaks memory until it crashes.
Rust pays for safety with compile-time mental overhead. Swift pays for safety with runtime CPU cycles and the risk of retain cycles.
Minimal example: Moving versus copying
Rust's default behavior is to move ownership. Swift's default behavior is to copy references. This difference shows up immediately in simple code.
/// Demonstrates Rust's move semantics.
fn main() {
// Allocate a String on the heap.
// `owner` holds the pointer and length.
let owner = String::from("data");
// Move ownership to `borrower`.
// The pointer transfers. `owner` is invalidated.
let borrower = owner;
// This line would fail with E0382 (use of moved value).
// The compiler knows `owner` is dead.
// println!("{}", owner);
// `borrower` is the sole owner now.
println!("{}", borrower);
}
In Rust, let borrower = owner transfers the pointer. owner is no longer valid. The compiler enforces this. If you try to use owner after the move, you get E0382. This prevents double-free bugs. If two variables thought they owned the same memory, both would try to free it when they dropped. Rust stops that at compile time.
Swift handles this differently.
// Swift uses ARC.
// `owner` holds a reference.
let owner = "data"
// `borrower` gets a reference.
// The reference count increases.
// Both `owner` and `borrower` are valid.
let borrower = owner
print(owner) // Works fine.
print(borrower) // Works fine.
In Swift, let borrower = owner increments the reference count. Both variables are valid. When borrower goes out of scope, the count decrements. When owner goes out of scope, the count decrements. If the count hits zero, the memory frees. This is convenient. You can pass data around without worrying about who owns it. But the counter updates happen at runtime. In a tight loop, millions of reference count updates add up. Rust has no such overhead. The pointer moves once, and that is it.
Realistic example: Shared state
Sometimes you need multiple parts of your program to access the same data. Swift handles this naturally with references. Rust requires you to be explicit.
use std::rc::Rc;
/// Shared ownership via reference counting.
fn main() {
// Wrap value in Rc. Counter starts at 1.
// The data lives on the heap.
let shared = Rc::new(vec![1, 2, 3]);
// Clone the Rc, not the vector.
// Counter becomes 2.
// Convention: use Rc::clone to signal shallow copy.
let shared2 = Rc::clone(&shared);
// Both can read the data.
assert_eq!(*shared, vec![1, 2, 3]);
assert_eq!(*shared2, vec![1, 2, 3]);
// When `shared2` drops, counter goes to 1.
// When `shared` drops, counter goes to 0 and memory frees.
}
Rust's Rc<T> stands for Reference Counted. It works like Swift's ARC, but you have to opt in. You wrap the value in Rc. You call Rc::clone to create a new handle. The community convention is to write Rc::clone(&shared) instead of shared.clone(). Both compile. Both work. The explicit form signals to readers that you are cloning the reference, not the underlying data. A deep clone would copy the entire vector. A shallow clone just bumps the counter. The explicit call prevents confusion.
Swift hides this behind the scenes. Every class instance is reference counted. You don't wrap it. You don't call a clone method. You just assign it. The runtime handles the rest. This makes Swift code cleaner for shared state. It also means you cannot accidentally create a value type that copies data when you meant to share it. Rust forces you to choose. Vec<T> copies. Rc<Vec<T>> shares. The type system makes the intent visible.
Rust makes you pay for shared ownership explicitly. Swift hides the cost behind the scenes.
Concurrency: Traits versus actors
Rust and Swift both aim to prevent data races, but they use different mechanisms. Rust uses traits. Swift uses actors.
Rust checks thread safety at compile time using the Send and Sync traits. Send means a type can be moved to another thread. Sync means a type can be shared between threads. The compiler derives these traits automatically for most types. If a type contains a raw pointer or a non-thread-safe component, the compiler marks it as not Send or not Sync. You cannot pass it across threads. If you need to share mutable state, you must wrap it in a Mutex or RwLock. The compiler checks that you lock the mutex before accessing the data.
Swift uses actors to isolate state. An actor is a type that serializes access to its mutable state. You can only access actor state via async calls. This prevents data races because only one task can mutate the actor at a time. Swift also has Sendable types, which are types that can be safely shared across concurrency domains. The compiler checks Sendable conformance. If a type is not Sendable, you cannot pass it to an async context.
Rust's approach is more flexible. You can share state synchronously using locks. You can move state between threads without async overhead. Swift's approach is simpler for async code. Actors prevent data races by design. But actors require async access. If you need synchronous access, you have to use locks or other synchronization primitives.
Rust gives you fine-grained control over concurrency. Swift gives you high-level isolation.
Error handling: Values versus flow
Rust treats errors as values. Swift treats errors as exceptional flow.
Rust uses the Result<T, E> type. Functions return Result. The caller must handle the error using match or the ? operator. The ? operator propagates the error up the call stack. If the caller cannot handle the error, it returns a Result itself. This forces error handling at every level. You cannot ignore an error. If you try to ignore a Result, the compiler warns you. You must use let _ = ... to discard it, which signals that you considered the value and chose to drop it.
Swift uses throws and try. Functions mark parameters or return values with throws. Callers use try to invoke the function. If the function throws, the error propagates. Callers can use try? to convert the result to an optional, ignoring the error. Callers can use try! to force the result, crashing if an error occurs. This makes Swift code more concise. You can ignore errors when you don't care. But it also means errors can be hidden. A try? might silently fail, and you won't know until runtime.
Rust's approach is stricter. You must handle errors. This leads to more robust code. Swift's approach is more flexible. You can choose how to handle errors. This leads to more concise code. But it requires discipline to avoid hiding bugs.
Rust forces you to confront errors. Swift lets you decide when to confront them.
Pitfalls and compiler errors
Rust's borrow checker fights you when you try to do unsafe things. Swift's compiler fights you when you try to do type-unsafe things.
If you try to mutate and read a value simultaneously in Rust, the compiler rejects you with E0502 (cannot borrow as mutable because it is also borrowed as immutable). This prevents data races and undefined behavior. Swift allows this. You can have multiple references to a mutable object. You must manage concurrency yourself.
If you try to use a value after moving it in Rust, the compiler rejects you with E0382 (use of moved value). This prevents double-free bugs. Swift does not have this issue. References are copied. The value stays alive as long as any reference exists.
If you try to create a retain cycle in Swift, the compiler does not warn you. You get a memory leak. Rust prevents cycles. You cannot create a cycle with strong references. You must use Weak references to break cycles. The compiler enforces this.
Rust stops you from writing code that leaks memory or races. Swift stops you from crashing, but you can still leak memory if you create a cycle.
Decision matrix
Use Rust for cross-platform systems programming where you need a single binary that runs on Linux, Windows, macOS, and embedded targets without a runtime dependency.
Use Rust for performance-critical libraries where every cycle counts and you cannot afford the overhead of reference counting or a garbage collector.
Use Rust when you want compile-time guarantees against data races, null pointer dereferences, and buffer overflows, even if it means wrestling with the borrow checker.
Use Rust when you are writing kernels, device drivers, or low-level infrastructure where you need fine-grained control over memory layout and allocation.
Use Swift for native Apple ecosystem development where you need access to Cocoa, SwiftUI, and the full suite of Apple frameworks.
Use Swift when you value developer ergonomics and rapid iteration over the strictest possible memory safety guarantees, accepting that reference counting adds runtime cost.
Use Swift when your team is already fluent in Swift and you want to leverage the extensive Apple documentation and community resources.
Use Swift when you are building an iOS, macOS, watchOS, or tvOS application and need the best integration with Apple's tools and services.
Rust demands you respect the hardware. Swift lets you focus on the interface. Choose based on where your constraints live.