A Systems Programmer's Case for Rust
September 27, 2025
For me, the main thing is that it’s a modern language. It’s a language that was designed to fix all the warts that we’ve had in all the other languages.
— Andreas Jung, Rust Core Team Member.
Introduction
For decades, the world of systems programming has been dominated by a single titan: C++. Its promise of “zero-cost abstractions” and direct hardware access has made it the default choice for performance-critical domains, from game engines and operating systems to high-frequency trading. Yet, this power comes at a price—a notoriously steep learning curve and a constant, looming threat of memory-related bugs and security vulnerabilities that have plagued the industry for years.
This has led to a fundamental trade-off: choose a language like C++ for raw performance at the cost of safety and complexity, or opt for a garbage-collected language like Java or Go for safety and simplicity at the cost of performance and predictability.
Rust, a language that began as a personal project by Graydon Hoare at Mozilla Research in 2006, fundamentally challenges this trade-off. It is designed to provide the low-level control and bare-metal performance of C++ while guaranteeing memory safety and data-race-free concurrency at compile time. This is not an incremental improvement; it is a paradigm shift in how we can approach building reliable, high-performance software. This post will provide a technical overview of Rust’s core value propositions, compare its design decisions to those of other major languages, and explore why it’s rapidly moving from a niche interest to a strategic choice for some of the world’s largest technology companies.
Table of Contents
- Introduction
- Table of Contents
- Data, Behavior, and Types: A New Way of Thinking
- A Critique of Object-Oriented Programming and C++’s Design Philosophy
- The Ownership Model: A Paradigm Shift in Memory Safety
- A Modern Development Experience: Abstractions and Tooling
- Industry Adoption: From Theory to Production Code
- Who is Rust For? Identifying the Ideal Use Cases
- Conclusion: A New Baseline for Systems Programming
Data, Behavior, and Types: A New Way of Thinking
Many mainstream languages like Java and C++ are built around classical Object-Oriented Programming (OOP), where data and behavior are tightly coupled within objects that inherit from one another.
Rust takes a different approach, drawing inspiration from functional programming and type theory to favor a data-oriented design based on composition over inheritance.
What is a “Type”? From Data to Information
In systems programming, we often think of a type as just a description of data in memory (e.g., int is 4 bytes). In functional programming and type theory, a type is a much richer concept: it’s a formal way of classifying values and expressing constraints on them.
A type system is a tool for reasoning about your program’s correctness before it runs.
Haskell represents the pinnacle of this pure, academic approach, with an extremely powerful type system that can prove complex properties about a program at compile time. However, this often comes at the cost of being disconnected from the low-level realities of hardware. C++, on the other hand, is all about the hardware, but its type system is comparatively weak at enforcing high-level invariants.
Rust strikes a pragmatic balance. It has a rich type system inspired by Haskell (e.g., algebraic data types, traits) but is fundamentally designed for systems programming. It uses types not just to describe memory layouts, but to enforce high-level rules about resource management, concurrency, and program state.
Data is Just Data: structs and enums
In Rust, the primary tools for modeling your domain are structs and enums. Important note: they are used to hold data, and nothing else.
Structs: Simple aggregations of data. They are like
Cstructs orC++structs without methods defined inside them. They just hold data.struct Player { name: String, health: i32, level: u32, }Enums (Algebraic Data Types): Rust’s enums are far more powerful than their
C/C++counterparts. They are Algebraic Data Types (ADTs), meaning each variant can hold different data. This allows you to encode program state in the type system itself.enum WebEvent { PageLoad, // Variant with no data KeyPress(char), // Variant with a tuple Click { x: i64, y: i64 }, // Variant with a struct }
Making Impossible States Impossible: match vs. switch
The match statement used with enums is exhaustive: the compiler will error if you forget to handle a variant. This is a significant safety improvement over the C++ std::variant or Go’s interface{} with a type switch. Let’s consider handling requests in a simple network server.
Rust’s Compile-Time Guarantee:
enum Request {
Get(String),
Post(String, String),
Delete(String),
// Let's add a new request type later: Put(String, String)
}
fn handle_request(req: Request) {
match req {
Request::Get(path) => println!("GET {}", path),
Request::Post(path, body) => println!("POST {}: {}", path, body),
// Whoops! We forgot to handle the Delete variant.
}
}
This code will simply not compile. The Rust compiler stops you with a clear error:
error[E0004]: non-exhaustive patterns: Delete(_) not covered
This forces the developer to account for all possibilities, preventing entire classes of bugs. The correct, exhaustive version would handle all variants. This becomes invaluable when you refactor: if you add a new Request::Put variant to the enum, the compiler will instantly show you every single match statement in your codebase that needs to be updated.
For more details, see the official Rust documentation for error[E0004]..
Go’s Runtime Hopefulness:
Go uses interface{} and type switches to achieve similar polymorphism, but the check is at runtime, not compile time (like in Rust).
type GetRequest struct { Path string }
type PostRequest struct { Path, Body string }
type DeleteRequest struct { Path string }
// If we add a PutRequest struct later, the compiler won't warn us.
func handleRequest(req interface{}) {
switch r := req.(type) {
case GetRequest:
fmt.Printf("GET %s\n", r.Path)
case PostRequest:
fmt.Printf("POST %s: %s\n", r.Path, r.Body)
// We forgot to handle DeleteRequest.
default:
// This default case might not be what we want.
// If we forget it, the program just does nothing for that case.
fmt.Println("Unknown request type")
}
}
This code compiles perfectly. If it receives a DeleteRequest, it will either do nothing or hit a default case, hiding a bug that Rust would have caught.
C++’s Verbose Visitor:
C++ std::variant is a significant improvement over C-style unions, but ensuring exhaustiveness is less ergonomic than Rust’s match. A common pattern is std::visit.
#include <variant>
#include <string>
struct GetRequest { std::string path; };
struct PostRequest { std::string path, body; };
struct DeleteRequest { std::string path; };
using Request = std::variant<GetRequest, PostRequest, DeleteRequest>;
void handle_request(const Request& req) {
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, GetRequest>) {
// handle Get
} else if constexpr (std::is_same_v<T, PostRequest>) {
// handle Post
}
// No compile error for forgetting DeleteRequest.
// Advanced template magic can be used to check for exhaustiveness,
// but it's not a built-in guarantee of the language's control flow.
}, req);
}
Like Go, the C++ version compiles without complaint, silently ignoring the unhandled DeleteRequest. While modern C++ provides tools to build compile-time checks for this, they are not a fundamental, out-of-the-box feature of the switch or if-constexpr constructs. Rust’s match integrates this safety check directly and simply.
See Chapter 5 (“Using Structs to Structure Related Data”) and Chapter 6 (“Enums and Pattern Matching”) of the Rust Book for more, as well as Section 3 (“Custom Types”) of Rust by Example (RBE).
Behavior as Traits: Composition Over Inheritance
The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
— Joe Armstrong, creator of Erlang.
In other words, in an object-oriented language such as Java or C++, if a hypothetical Gorilla class inherits from a JungleAnimal class, it drags all that parent complexity with it. If all you need is the eat_banana() behavior, you’re forced to accept the entire jungle. This often leads to the “brittle base class” problem, where a change in a parent class can unexpectedly break child classes in subtle ways.
Instead of methods living inside a class, Rust separates them. You define behavior in impl blocks, often by implementing traits. A trait is a collection of method signatures that defines a shared capability or concept (e.g., Draw, Clone, Debug). It is an interface that a type can choose to implement.
This is fundamentally about composition. You start with simple data structures (structs and enums) and then compose behaviors onto them by implementing traits. This is far more flexible than inheritance.
For example, in embedded programming, you could model a GPIO pin like this:
// The TYPE: simple data describing a pin
pub struct GpioPin {
port: char,
pin_number: u8,
}
// The BEHAVIORS, defined as separate traits
pub trait GpioWrite {
fn set_high(&mut self);
fn set_low(&mut self);
}
pub trait GpioRead {
fn is_high(&self) -> bool;
}
// The IMPLEMENTATION: applying a behavior to a type
impl GpioWrite for GpioPin {
fn set_high(&mut self) { /* hardware-specific code to set pin high */ }
fn set_low(&mut self) { /* hardware-specific code to set pin low */ }
}
Here, a GpioPin is just data, it does include any “behavioral” code. We grant it the ability to be written to by implementing the GpioWrite trait for it. If it were also a readable pin, we could simply add another impl GpioRead for GpioPin. This avoids the rigid hierarchies of OOP, where you might be forced to create awkward classes like WriteOnlyPin or ReadWritePin.
See Chapter 10 (“Generic Types, Traits, and Lifetimes”) of The Rust Book for more on this (specifically Section 10.2.), as well as Section 16 (“Traits”) of Rust by Example (RBE).
A Critique of Object-Oriented Programming and C++’s Design Philosophy
The trait-based approach stands in stark contrast to the design of languages like C++ and Java, and indeed, to the entire OOP paradigm as it is commonly practiced.
The Failures of the OOP Dream
The promise of OOP was reusable, modular code through inheritance. Born from innovative ideas in languages like Simula and Smalltalk for modeling complex systems, it was popularized by C++ and Java in the 80s and 90s as the definitive solution for large-scale software engineering. The vision was an industrial one: build software from interchangeable, component-like objects. The reality, however, has often been brittle, complex, and deeply coupled systems.
The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
— Joe Armstrong, creator of Erlang.
Coming back to this quote, it perfectly captures the problem of such deep, rigid inheritance hierarchies. If a Gorilla class inherits from a JungleAnimal class, which inherits from Mammal, it drags all that parent complexity with it. This creates several problems:
The Brittle Base Class Problem: A seemingly innocuous change in a parent class can unexpectedly break child classes in subtle ways. The tight coupling between parent and child makes the system fragile.
Inflexible Hierarchies: Real-world concepts don’t always fit into neat tree structures. What if you want an object that is both a
Vehicleand aBuilding(like a mobile home)? OOP forces you into convoluted patterns like multiple inheritance (C++) or interfaces with default methods (Java), which are often (to put it lightly) clumsy workarounds.Encapsulation Breakdown: True encapsulation is about hiding implementation details. But in many OOP designs, inheritance requires the child class to know intimate details about the parent’s implementation, violating this core principle.
Rust’s trait system avoids this entirely. You don’t inherit a “jungle”; you simply implement the EatsBananas trait for your Gorilla struct. The data and behavior are decoupled, allowing for maximum flexibility and true modularity.
The Functional Programming Detour: Purity at a Price
As the limitations of mainstream OOP became more apparent, another school of thought, rooted in academia and mathematics, offered a different path: functional programming (FP). Languages like Lisp, Scheme, and later OCaml and Haskell, proposed a radical alternative. To make these concepts concrete, let’s look at some Haskell examples, using a (simplified) Request type for a web server.
-- A simple data type to represent a request
data Request = Get { path :: String } | Post { path :: String, body :: String }
Immutability:
Data structures are unchangeable. Instead of modifying data, you create new data with the desired changes. An OOP programmer might think of request.setPath("/new"), which mutates the object. In Haskell, that’s impossible.
-- This function takes a request and a new path, and returns a NEW request.
-- The original is untouched.
updatePath :: Request -> String -> Request
updatePath (Get oldPath) newPath = Get newPath
updatePath (Post oldPath body) newPath = Post newPath body
“Pure” Functions:
updatePath above is a perfect example of a pure function. It’s a mathematical mapping from inputs to outputs. For the same Request and String, it will always produce the same new Request. It has no observable side effects like modifying global state, printing to the console, or performing I/O. This makes code incredibly easy to reason about and test.
First-Class Functions:
Functions are values, just like numbers or strings. They can be passed as arguments to other functions. This allows for powerful abstractions.
-- A higher-order function that takes a processing function and applies it to a list of requests.
processRequests :: (Request -> String) -> [Request] -> [String]
processRequests processor requests = map processor requests
-- A simple function that extracts the path. It can be passed as an argument.
extractPath :: Request -> String
extractPath (Get p) = p
extractPath (Post p _) = p
main_processor = do
let reqs = [Get "/home", Post "/login" "user=admin"]
-- We pass the `extractPath` function into `processRequests`.
let paths = processRequests extractPath reqs
print paths -- This will print ["/home", "/login"]
Monads: Quarantining Side Effects
The processRequests and extractPath functions are pure. But what about print paths? That’s a side effect (I/O). Haskell uses advanced type-system constructs like monads to manage this. The do block in main_processor signals that we are sequencing “side-effectful” actions within the IO monad. In a way, monads allow programmers to explicitly sequence these “impure” operations in a controlled, contained manner, emulating some of the encapsulation benefits of OOP without its pitfalls of implicit state and inheritance.
This approach eliminates entire classes of bugs related to shared mutable state. However, this purity comes with its own set of trade-offs, especially for systems programming. The heavy reliance on immutability can lead to performance challenges if not managed carefully, and the high level of abstraction can obscure the underlying hardware realities of memory layout and control flow. For many systems developers, the purely functional world felt powerful but impractical for writing device drivers or game engines.
This created a chasm: on one side, the unsafe, complex, but hardware-centric world of C++; on the other, the safe, elegant, but abstract world of Haskell. It is precisely this chasm that Rust was designed to bridge.
For a deeper dive into Rust’s functional programming features, see Chapter 13 (“Functional Language Features: Iterators and Closures”) of The Rust Book.
C++: The Swiss Army Knife with 200 Dull Blades
The issues with OOP are compounded in C++ by its design philosophy, which can be summarized as “include everything and let the user figure it out.” This has led to a language of immense, arguably unnecessary, complexity. Unix pioneer Ken Thompson, co-creator of C, had a particularly sharp critique:
It certainly has its good points. But by and large I think it’s a bad language. It does a lot of things half well and it’s just a garbage heap of ideas that are mutually exclusive… It’s way too big, way too complex. And it’s obviously built by a committee. Stroustrup campaigned for years and years and years… to get it adopted and used. And he sort of ran all the standards committees with a whip and a chair. And he said “no” to no one. He put every feature in that language that ever existed. It wasn’t cleanly designed—it was just the union of everything that came along. And I think it suffered drastically from that.
— Ken Thompson, Unix pioneer and co-creator of C.
This describes a language that, in trying to please everyone, created a minefield. C++ has multiple ways to do almost everything (e.g., at least five forms of initialization, unique_ptr vs. shared_ptr vs. raw pointers), and the “correct” choice is often subtle and context-dependent. This leads to the “subset” problem: every organization uses a different, mutually incompatible subset of C++, making code portability a nightmare. C++ isn’t a coherent language; it’s a collection of features bolted together over decades.
The Ownership Model: A Paradigm Shift in Memory Safety
The heart of Rust’s value proposition is its ownership model, a novel approach to memory management that operates entirely at compile time. To appreciate its significance, one must contrast it with the two dominant paradigms: manual memory management (C/C++) and automatic garbage collection (Java/Go).
Ownership: Deterministic Resource Management
In Rust, every value has a single “owner.” When the owner goes out of scope, the value is “dropped,” and its resources (memory, file handles, network sockets) are freed. This deterministic, scope-based resource management is known as RAII (Resource Acquisition Is Initialization), a pattern familiar to C++ developers.
However, Rust makes two crucial changes:
- First, it’s universal and non-negotiable.
- Second, when a value is assigned to another variable or passed to a function, its ownership is moved. The original variable is now considered uninitialized and cannot be used, a rule enforced at compile time. This elegantly prevents “double frees” and makes the flow of ownership explicit.
See Section 15.1. (“RAII”) of Rust by Example (RBE) for more.
C++ Example (RAII with std::unique_ptr):
#include <iostream>
#include <memory>
#include <string>
void process_string(std::unique_ptr<std::string> s) {
std::cout << "Processing: " << *s << std::endl;
} // `s` is destroyed here, memory is freed.
int main() {
auto my_string = std::make_unique<std::string>("hello");
process_string(std::move(my_string));
// std::cout << *my_string; // Compile Error! my_string was moved.
}
Rust Equivalent (Ownership Move):
fn process_string(s: String) {
println!("Processing: {}", s);
} // `s` goes out of scope and is dropped here.
fn main() {
let my_string = String::from("hello");
process_string(my_string);
// println!("{}", my_string); // Compile Error! Value borrowed after move.
}
While both prevent use-after-move, Rust’s ownership is fundamental. This contrasts with garbage-collected languages like Go, where the programmer has no control over when memory is freed, leading to potential non-determinism in resource cleanup and performance.
For a full breakdown, see Chapter 4 (“Understanding Ownership”) of The Rust Programming Language Book, as well as Section 15.2. (“Ownership and moves”) of Rust by Example (RBE).
Borrowing: Enforcing Data Discipline
Moving ownership constantly would be impractical. Rust’s solution is borrowing, which allows parts of the code to access data via references without taking ownership. The borrow checker, Rust’s most famous feature, enforces a critical set of rules at compile time:
You can have ANY number of immutable references (
&T) simultaneously.You can have ONLY ONE mutable reference (
&mut T).A mutable reference cannot exist at the same time as any immutable references.
This “aliasing XOR mutability” rule eliminates entire categories of bugs, from simple data corruption to complex data races in concurrent code.
C++ Use-After-Free via Iterator Invalidation:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3};
int& first = v[0]; // Create a reference to the first element.
// This push_back might cause the vector to reallocate its memory,
// invalidating all existing references.
v.push_back(4);
// `first` is now a dangling reference. Accessing it is undefined behavior.
std::cout << "First element is: " << first << std::endl; // Potential crash!
}
This code compiles but can crash or produce garbage data at runtime. The responsibility to avoid this lies entirely with the programmer.
Rust’s Compile-Time Prevention:
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // Create an immutable borrow.
// This line will NOT compile.
v.push(4); // ERROR: cannot borrow `v` as mutable because it is also borrowed as immutable
// The compiler stops us long before this line is ever reached.
// println!("First element is: {}", first);
}
The borrow checker sees that v.push() requires a mutable borrow of v while first holds an immutable borrow. It rejects the program, preventing the possibility of the bug.
See Section 15.3. (“Borrowing”) of Rust by Example (RBE) for more on this.
Lifetimes: Eliminating Dangling Pointers
The final piece is ensuring references never outlive the data they point to. The compiler achieves this through lifetime analysis. In most cases, lifetimes are inferred automatically. When ambiguity arises, the programmer provides explicit lifetime annotations.
C++ Dangling Pointer:
const std::string& get_longest(const std::string& s1, const std::string& s2) {
if (s1.length() > s2.length()) {
return s1;
} else {
std::string temp = "longer"; // A local variable
return temp; // Returns a reference to `temp`, which is destroyed here!
}
}
The above code is a ticking time bomb (waiting to ruin your weekend) that compiles but returns a reference to memory that has been deallocated.
Rust’s Compile-Time Prevention (and Solution):
// This function signature tells the compiler that the returned reference
// must live at least as long as the SHORTEST of the two input references.
// The `'a` is a lifetime parameter.
fn get_longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
// This works because both s1 and s2 are valid here.
result = get_longest(&string1, &string2);
println!("Inside scope, longest is: {}", result); // This is fine.
}
// println!("The longest string is {}", result); // COMPILE ERROR! `string2` does not live long enough.
}
The Rust compiler understands that string2 is destroyed at the end of the inner scope. It sees that result could potentially be a reference to string2, and therefore flags the final println! as an error because result would be a dangling pointer. This entire class of bugs is eliminated at compile time. How cute is that?
For a deep dive, see Chapter 10 (“Generic Types, Traits, and Lifetimes”) of The Rust Book, as well as Section 15.4. (“Lifetimes”) of Rust by Example (RBE).
A Modern Development Experience: Abstractions and Tooling
While memory safety is the headline feature, it’s the modern developer experience that often wins “converts”.
World-Class Tooling: Cargo and the Ecosystem
Rust comes with Cargo, an integrated package manager and build system that is nothing short of revolutionary for developers (read “blasphemers”) coming from C++. It handles:
Dependency Management: The
Cargo.tomlmanifest file is a simple, declarative way to specify dependencies from the central crates.io repository. This stands in stark contrast to the fragmented C++ ecosystem ofConan,vcpkg, and manual library management.Reproducible Builds: Cargo automatically generates a
Cargo.lockfile, ensuring that every developer on a project, and the CI server, gets the exact same version of every dependency, eliminating “works on my machine” issues.Integrated Toolchain: A single cargo command can build your project (
cargo build), run tests (cargo test), generate test coverage (cargo tarpaulin), run benchmarks (cargo bench), generate documentation (cargo doc), format code (cargo fmt), and run a powerful static analyzer (cargo clippy). This consistency is a massive productivity boost compared to orchestratingMakefiles,CMake,Doxygen, andClang-Tidy.
See Chapter 7 (“Managing Growing Projects with Packages, Crates, and Modules”) and Chapter 14 (“More About Cargo and Crates.io”) of The Rust Book, as well as Section 12 of Rust by Example (RBE) for more.
A Precise GUI Example: OOP vs. Data-Oriented
Let’s make the GUI example more concrete to see the practical difference.
Typical OOP Approach (e.g., in Java/C#):
Imagine a GUI library where everything must inherit from a Widget base class.
// A rigid hierarchy. What if we want a non-drawable, clickable region?
// You're forced to create an "InvisibleWidget" class.
public abstract class Widget {
protected int x, y, width, height;
public abstract void draw(Screen s);
// Maybe event handling is here too? Tightly coupled.
public boolean onEvent(Event e) { return false; }
}
public class Button extends Widget {
private String label;
@Override
public void draw(Screen s) { /* Draw button with label */ }
@Override
public boolean onEvent(Event e) {
if (e instanceof ClickEvent) { /* Handle click */ return true; }
return false;
}
}
public class Image extends Widget {
private byte[] imageData;
@Override
public void draw(Screen s) { /* Draw image */ }
// An image isn't clickable, but it inherits the onEvent method anyway.
}
Rust’s Trait-Based Approach:
// 1. Define distinct behaviors as traits.
pub trait Draw {
fn draw(&self, screen: &mut Screen);
}
pub trait EventHandler {
fn on_event(&mut self, event: Event) -> bool;
}
// 2. Define simple, independent data structures.
pub struct Button {
pub label: String,
pub clicked: bool,
}
pub struct Image {
pub data: Vec<u8>,
}
// 3. Compose behaviors onto the data structures as needed.
impl Draw for Button {
fn draw(&self, screen: &mut Screen) { /* Draw the button */ }
}
impl EventHandler for Button {
fn on_event(&mut self, event: Event) -> bool {
if let Event::Click {..} = event {
self.clicked = true;
return true;
}
false
}
}
impl Draw for Image {
fn draw(&self, screen: &mut Screen) { /* Draw the image */ }
}
// Note: Image does NOT implement EventHandler. It doesn't have that capability.
// 4. Use traits to write generic code.
// This function can operate on any collection of drawable items.
fn draw_all_widgets(widgets: &[&dyn Draw]) {
for widget in widgets {
widget.draw(&mut Screen::new());
}
}
This is far more flexible. The data (Button, Image) is decoupled from the behavior (Draw, EventHandler). An Image doesn’t carry the dead weight of an event-handling method it will never use. If we need a new kind of interactive element, we just implement the EventHandler trait for it, without needing to fit it into a rigid Widget hierarchy.
More on Rust’s “Object-Oriented Programming (OOP)” features in Chapter 18 (“Object-Oriented Programming Features of Rust”) of The Rust Book.
Robust Error Handling: Result vs. Exceptions and nil
Rust eschews traditional exceptions, which can create invisible control flow paths and make it difficult to reason about a program’s behavior. Instead, recoverable errors are handled explicitly through the Result<T, E> enum. A function that can fail has this possibility encoded directly in its return type. It will return either Ok(T) with the success value of type T, or Err(E) with an error value of type E. Most importantly, the compiler forces you to handle the Err case, making it impossible to accidentally ignore a potential failure.
Explicit Handling with match
Let’s look at a function for safe division. Instead of crashing on division by zero, it returns a Result.
fn safe_divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0.0 {
// On failure, return an Err variant with an error message.
Err(String::from("Cannot divide by zero"))
} else {
// On success, return an Ok variant with the result.
Ok(numerator / denominator)
}
}
fn main() {
let result = safe_divide(10.0, 2.0);
match result {
Ok(value) => println!("Result: {}", value),
Err(e) => println!("Error: {}", e),
}
let error_result = safe_divide(10.0, 0.0);
// If you tried to use error_result without handling the Err case,
// the compiler would stop you.
match error_result {
Ok(value) => println!("Result: {}", value),
Err(e) => println!("Error: {}", e),
}
}
In this example, the caller is forced by the match statement’s exhaustiveness rule (the same rule we saw with the Request enum) to handle both success (Ok) and failure (Err). You cannot simply “forget” to check for an error. This makes the code far more robust.
Now that we understand the basics, let’s see how this compares to other languages and how Rust makes it more ergonomic.
Go’s if err != nil Boilerplate:
file, err := os.Open("foo.txt")
if err != nil {
return nil, err
}
// more error checks...
C++/Java’s Invisible Control Flow:
try {
// several function calls...
// An exception could be thrown from anywhere, it's not visible in the types.
} catch (IOException e) {
// Handle error
}
Rust’s ? Operator:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
// The '?' operator propagates the error automatically. If File::open fails,
// the function immediately returns the Err variant.
let mut file = File::open("username.txt")?;
let mut username = String::new();
// The '?' operator does the same for the read operation.
file.read_to_string(&mut username)?;
// If both operations succeed, return the username wrapped in Ok.
Ok(username)
}
The ? operator provides concise error propagation that is still explicit and type-checked, offering the best of both worlds: less boilerplate than Go and more safety and clarity than C++/Java exceptions.
See Chapter 9 (“Error Handling”) of The Rust Book, as well as Section 18 (“Error handling”) of Rust by Example (RBE) for more.
Industry Adoption: From Theory to Production Code
The benefits of Rust are not just theoretical. Some of the world’s largest technology companies are adopting it for critical, performance-sensitive systems, often replacing existing C++ codebases.
Google: Rust is a first-class language for systems programming in the Android Open Source Project. In a 2022 blog post, Google stated that since introducing Rust, “there have been zero memory safety vulnerabilities discovered” in their new Rust code. They also reported that based on internal data, “Rust developer productivity and satisfaction is high,” with developers being twice as productive as their C++ counterparts after overcoming the initial learning curve. Read more on the Google Security Blog.
Microsoft: Recognizing that approximately 70% of their yearly security patches are fixes for memory safety bugs in C and C++ code, Microsoft has made a strategic bet on Rust. They are actively rewriting core Windows components, including parts of the kernel, in Rust and have an official “Rust for Windows” project to provide seamless interoperability with the Windows API.
Amazon Web Services (AWS): AWS has used Rust to build some of its most critical and performance-sensitive infrastructure. Firecracker, the virtualization technology that powers AWS Lambda and Fargate, is written in Rust, leveraging its safety and speed for secure multi-tenant isolation.
The Linux Kernel: Perhaps the most significant endorsement for any systems language, Rust was officially accepted for kernel development in 2022, joining the exclusive ranks of C and assembly. This is a milestone that C++, despite its decades of existence, has never achieved (I believe this is what is referred to as “skill issue” nowadays). The reasons for C++’s exclusion were articulated very CLEARLY by Linus Torvalds in a now-famous 2004 mailing list post:
In fact, in Linux we did try C++ once already, back in 1992. It sucks. Trust me - writing kernel code in C++ is a BLOODY STUPID IDEA. The fact is, C++ compilers are not trustworthy. They were even worse in 1992, but some fundamental facts haven’t changed: the whole C++ exception handling thing is fundamentally broken. It’s especially broken for kernels. any compiler or language that likes to hide things like memory allocations behind your back just isn’t a good choice for a kernel. you can write object-oriented code (useful for filesystems etc) in C, without the crap that is C++.
In general, I’d say that anybody who designs his kernel modules for C++ is either (a) looking for problems (b) a C++ bigot that can’t see what he is writing is really just C anyway (c) was given an assignment in CS class to do so.
Feel free to make up (d). Linus
Rust, with its predictable performance, explicit error handling (e.g. via
Result), and lack of hidden memory allocations, directly addresses these long-standing concerns. Its successful integration for writing new drivers and subsystems is a powerful testament to its suitability for the most demanding software environments in the world.
Who is Rust For? Identifying the Ideal Use Cases
Rust is not a silver bullet, but its unique combination of features makes it an ideal choice for a wide range of applications:
Embedded Systems & OS Development: Where direct hardware access and a predictable memory footprint are non-negotiable.
Web Backends & Network Services: Where performance, security, and robust concurrency are critical for handling thousands of simultaneous connections.
Command-Line Tools: Where fast startup times and a small binary size are highly valued.
WebAssembly (WASM): Rust is a first-class language for WASM, allowing developers to run safe, high-performance code in the browser.
Anyone who enjoys this kind of programming :)
Conclusion: A New Baseline for Systems Programming
Rust represents a significant evolution in programming language design. It proves that a language does not have to sacrifice safety for performance, or developer experience for low-level control. By providing memory safety, fearless concurrency, and a world-class toolchain, it empowers developers to build software that is faster, more reliable, and more secure. While C++ will remain a cornerstone of the industry for years to come, Rust offers a compelling, modern, and, above all, safer path forward for the next generation of systems software.