The Borrow Checker Says No
You are building a high-performance image processor. You have a buffer of pixels in memory. You need to swap two pixels in place to sort them by brightness. You write a function that takes two mutable references to the buffer. The compiler rejects you with E0502 (cannot borrow as mutable because it is also borrowed as immutable). You try to restructure the code. The borrow checker blocks every path. The safe API simply cannot express the operation you need.
You need to tell the compiler, "I know the rules. I know this is safe. Please let me do this."
That is what unsafe does. It is not a magic spell that makes your code crash. It is a waiver. It marks a block of code where you opt out of the compiler's automatic safety checks and take full responsibility for correctness.
What unsafe Actually Means
Rust guarantees memory safety by enforcing rules at compile time. The borrow checker ensures you never have dangling pointers, data races, or buffer overflows. These guarantees come from the compiler verifying your code against a strict set of invariants.
unsafe relaxes those invariants. When you write an unsafe block, you are telling the compiler to stop checking four specific categories of operations. The compiler still checks types, lifetimes of safe references, and logic errors. You can still get E0308 (mismatched types) inside an unsafe block. unsafe does not disable the compiler. It only disables the safety checks that require runtime guarantees the compiler cannot verify.
Think of it like a library with strict rules. The librarian will not let you take a book off the shelf if someone else is reading it. unsafe is the master key that lets you sneak into the restricted section. You can take the book, move it, or drop it on the floor. The librarian will not stop you. If you drop the book and break the spine, the mess is entirely on you. The library system assumes you know what you are doing.
The code inside unsafe can still be perfectly safe. The difference is that the safety is not verified by the compiler. It is verified by you, the programmer, and anyone who reviews your code.
The Four Operations
unsafe allows exactly four things that are otherwise forbidden. If you are using unsafe for anything else, you are probably doing it wrong.
Dereferencing raw pointers is the first category. Raw pointers (*const T or *mut T) do not carry lifetime or validity information. The compiler cannot check if they point to valid memory. Dereferencing them requires unsafe. You might point to freed memory, uninitialized stack space, or a completely wrong address. The compiler refuses to guess.
Calling extern functions is the second. Functions from other languages like C are marked extern. Rust cannot verify their behavior, their calling convention, or their memory usage. Calling them requires unsafe. The external function might modify global state, leak memory, or crash the process. Rust has no way to enforce its own rules across that boundary.
Accessing mutable static variables is the third. Static variables live for the entire program. Mutable statics can be accessed from anywhere, creating potential data races. Accessing static mut requires unsafe. Multiple threads can read and write the same global variable simultaneously. The compiler cannot track those concurrent accesses.
Implementing unsafe traits is the fourth. Some traits, like Send or Sync, make promises about thread safety. The compiler cannot verify those promises automatically. Implementing them requires unsafe. You are telling the type system that your custom struct can safely cross thread boundaries. If you are wrong, the program will race and corrupt memory.
If your unsafe block does not contain one of these four operations, you do not need unsafe. The compiler will complain if you put an unsafe block around safe code.
Minimal Example
Here is the simplest case: dereferencing a raw pointer.
fn main() {
let x = 42;
// Create a raw pointer. This is safe to do.
// The compiler trusts you not to misuse it.
let ptr = &x as *const i32;
// Dereferencing requires unsafe.
// The compiler cannot verify ptr is valid.
unsafe {
// SAFETY: ptr points to x, which is alive and valid.
let value = *ptr;
println!("Value: {}", value);
}
}
The unsafe block wraps only the dereference. Creating the pointer is safe. Reading the value is safe. The only unsafe operation is the dereference itself.
Convention aside: keep unsafe blocks as small as possible. The community calls this the "minimum unsafe surface" rule. A small block is easier to review. A small block makes it obvious what the programmer is promising. If you wrap an entire function in unsafe, reviewers have to scan hundreds of lines to find the actual unsafe operation. Isolate the risk.
Walkthrough
When the compiler sees unsafe, it changes its behavior for the four operations listed above. It stops emitting errors for raw pointer dereferences, extern calls, mutable static access, and unsafe trait implementations.
If you try to dereference a raw pointer outside an unsafe block, the compiler rejects you with E0133 (dereference of raw pointer requires unsafe block). This error is the gatekeeper. It forces you to acknowledge the risk.
Inside the unsafe block, the compiler still checks everything else. You cannot assign an i32 to a String. You cannot use a moved value. You cannot violate trait bounds. unsafe does not give you superpowers. It only removes the safety rails for specific memory-related operations.
Ah-ha moment: unsafe does not make your code faster. Many beginners think unsafe bypasses overhead. That is false. The compiler optimizes safe code aggressively. In fact, unsafe can sometimes make code slower. When you use unsafe, you remove invariants that the optimizer relies on. The optimizer loves guarantees. If you tell the compiler "I know this is safe" but you cannot prove it, the optimizer might have to be more conservative. unsafe is for capability, not performance. Use it when safe Rust cannot express what you need.
Realistic Example: Building a Safe Abstraction
The most important use of unsafe is building safe abstractions. The standard library is full of types that use unsafe internally to provide safe APIs. Vec, String, Rc, Mutex — they all use unsafe to manage memory or concurrency. You use unsafe to write the low-level logic, then you wrap it in a safe interface that the compiler can verify.
Here is a simple wrapper around a raw pointer. It ensures the pointer is never null and provides safe access.
use std::ptr::NonNull;
/// A safe wrapper around a raw pointer to a managed resource.
/// Guarantees the pointer is non-null and valid for the lifetime of the struct.
struct SafePointer {
ptr: NonNull<i32>,
}
impl SafePointer {
/// Creates a new SafePointer from a raw pointer.
///
/// # Safety
/// The pointer must be valid and point to a live i32.
/// The caller must ensure the i32 remains alive as long as this struct exists.
pub unsafe fn from_raw(ptr: *mut i32) -> Self {
// SAFETY: Caller guarantees the pointer is valid and non-null.
// We wrap it in NonNull to enforce non-nullness at the type level.
Self {
ptr: NonNull::new_unchecked(ptr),
}
}
/// Reads the value without moving it.
/// This is safe because the struct guarantees the pointer is valid.
pub fn get(&self) -> i32 {
unsafe {
// SAFETY: The pointer is valid for the lifetime of this struct.
// We only read, so no aliasing issues if the caller respects rules.
*self.ptr.as_ptr()
}
}
}
The from_raw function is unsafe. It requires the caller to prove the pointer is valid. The get function is safe. It uses unsafe internally, but the wrapper guarantees the invariants. Users of SafePointer never see unsafe. They call get() and get a value. The safety is encapsulated.
Convention aside: // SAFETY: comments are proofs. When you write an unsafe block, you must list the invariants that make it safe. If you cannot write the proof, you do not have the safety. Treat the comment as a contract. Reviewers will check the comment against the code. If the comment is vague, the code is suspect.
Convention aside: prefer NonNull over raw pointers for abstractions. NonNull is a type-level guarantee that the pointer is not null. It helps the compiler optimize and signals intent. Raw pointers can be null. NonNull cannot. Use NonNull when you are building a wrapper.
Treat the SAFETY comment as a proof. If you can't write it, you don't have one.
Pitfalls
Using unsafe introduces risks that safe Rust eliminates. The most dangerous risk is undefined behavior. UB means the compiler can assume anything. It might optimize away your null check. It might reorder your memory accesses. It might crash the program in a way that makes no sense. UB is the enemy.
Common pitfalls include dangling pointers, use-after-free errors, data races, aliasing violations, and forgotten invariants. You create a raw pointer to a value. The value is dropped. You dereference the pointer. The memory is garbage. UB. Two threads access the same memory without synchronization. One writes. UB. You create two mutable references to the same memory. The compiler assumes mutable references are unique. UB. You write an unsafe block but forget to maintain the invariants. The code works in testing but crashes in production.
Another pitfall is overusing unsafe. Every unsafe block increases the cognitive load for reviewers. It makes the code harder to reason about. It introduces a surface area for bugs. If you can solve the problem with safe Rust, do it. RefCell, Mutex, Cell, and interior mutability patterns often solve problems that seem to require unsafe.
Counter-intuitive but true: the more you use unsafe, the harder the rest of your code becomes to reason about. Safe code is self-documenting. The compiler guarantees correctness. unsafe code requires reading comments, understanding invariants, and trusting the programmer. Minimize unsafe to keep your codebase maintainable.
Trust the borrow checker. It usually has a point. If the compiler rejects your code, try to restructure the logic before reaching for unsafe. Often, the safe solution is just a different way of organizing the data.
Decision: When to Use unsafe
Use unsafe for FFI when you call C or another language and have to cross out of Rust's safety. The compiler cannot verify external functions. You must wrap the call in unsafe and provide a safe interface if possible.
Use unsafe for performance-critical inner loops where measured profiling shows safe abstractions are the bottleneck. Only use this after profiling. Only use this when the safe alternative is provably slower. Isolate the unsafe in a small helper function.
Use unsafe when you're implementing a safe abstraction yourself. A Vec, a linked list, an allocator, a custom smart pointer. Use unsafe internally to manage memory or invariants, then expose a safe API. The goal is to hide unsafe from users.
Reach for plain references when lifetimes are simple. The borrow checker handles most cases. unsafe is rarely worth it for simple data access.
Reach for RefCell or Cell when you need interior mutability. These types provide safe ways to mutate data behind shared references. They have runtime overhead, but they prevent UB.
Reach for Mutex or RwLock when you need thread-safe shared state. These types handle synchronization safely. unsafe concurrency is extremely hard to get right. Avoid it unless you are building a lock-free data structure.