Skip to main content

Rust Ownership and Side-Effect Safety

· 3 min read
Software Engineer
AI Writing Assistant

One of Rust’s most powerful features is its ownership model, which enforces memory safety and helps prevent bugs related to side effects. In this post, we’ll explore how Rust’s function signatures make side effects explicit—or impossible—by design, and compare this to common patterns in other languages where side effects are often hidden.

Hidden Side Effects

Let's start with a simple Java example where we have a class that holds an list of integers and a method that returns the sorted list.

List<Integer> sort(List<Integer> innerList) {
Collections.sort(innerList);
return innerList;
}

There is nothing in the signature or the function name to indicate that innerList will be changed. It may not have even been intentional.

Now imagine this as a small part of a larger codebase touched by many people. Suppose the code looks something like this:

void doMainThing() {
doSomeWork();
doSomeMoreWork();
}

void doSomeWork() {
List<Integer> sortedData = sort(innerList);
System.out.println(sortedData[0]);
doSomeMoreWork();
}

void doSomeMoreWork() {
binarySearch(innerList, 42);
}

There is a bug here that can be easily overlooked when multiple work on separate parts of the code. doSomeMoreWork() expects innerList to be sorted. After all, when doSomeMoreWork is called, it always is sorted. But it wasn't intended that way. So when someone else goes back and "fixes" the sort method, doSomeMoreWork will experience unexpected behavior because of the side effect.

Variations on this pattern are common and a frequent source of hard to track down bugs. But in Rust, we can entirely eliminate this class of bugs by making side effects explicit in function signatures.

Rust: Explicit Function Signatures

Now, let's build the same example in Rust. If a function needs to sort a vector in place, it must take a mutable reference:

fn sort(vec: &mut Vec<i32>) {
vec.sort();
}

The &mut in the signature makes it explicit that mutation is possible. The compiler ensures that only one mutable reference exists at a time, preventing data races and unexpected side effects.

Side Effect Free Rust

But if you intended no side effects, you'd have an immutable reference to the vector. You'd be forced to clone it and make the copy explicit.

fn sort(vec: &Vec<i32>) -> Vec<i32> {
let mut new_vec = vec.clone();
new_vec.sort();
new_vec
}

Here, the function cannot modify the original vector, and the compiler enforces this. You have a compile-time guarantee that sort is side-effect free with respect to its argument.

Immutable references are the default in Rust—to create a side effect you must declare that explicitly in the function signature.

Why This Matters

Rust’s approach makes code easier to reason about. When you see a function signature, you know whether mutation or side effects are possible. This leads to safer, more maintainable code, and helps prevent entire classes of bugs that are common in other languages.