What Is the Difference Between Methods and Associated Functions in Rust?

Methods operate on instances using `self`, while associated functions operate on the type itself without requiring an instance.

The blueprint vs. the built house

You're writing a Rectangle struct. You need a way to create a rectangle from width and height. You also need a way to calculate the area of an existing rectangle. In Python or JavaScript, you'd probably just write two functions inside a class and call them both on the instance. Rust draws a hard line between these two operations. One lives on the instance. The other lives on the type itself. Confusing them leads to compiler errors that feel like the language is fighting you. Understanding the split makes the borrow checker easier to reason about and your API design cleaner.

Think of a type like a blueprint for a house. An associated function is part of the blueprint. It tells you how to build the house or how to measure the standard lot size. You don't need a house to use the blueprint. You just need the blueprint itself. A method is something a built house can do. You can't ask the blueprint to open a door. You need an actual house with a door to open. In Rust code, the blueprint is the type name. The built house is an instance. Associated functions run on the type. Methods run on the instance.

The blueprint doesn't open doors. Build the house first.

How Rust draws the line

The divider is the self parameter. If a function inside an impl block takes self, &self, or &mut self as its first argument, it is a method. If it has no self parameter, it is an associated function. The syntax follows the parameter. Methods use dot notation: instance.method(). Associated functions use double colon notation: Type::function().

This isn't just syntax sugar. The compiler uses the presence of self to decide how to handle borrowing and ownership. When you call a method, Rust performs automatic referencing. When you call an associated function, Rust passes arguments exactly as declared. The distinction forces you to think about whether an operation requires existing state or creates new state.

The minimal split

Here is the smallest example showing both sides. The Point struct has an associated function to create it and a method to read its data.

struct Point {
    x: f64,
    y: f64,
}

impl Point {
    /// Associated function: constructs a Point.
    /// No instance exists yet, so there is no `self`.
    fn new(x: f64, y: f64) -> Point {
        Point { x, y }
    }

    /// Method: operates on an existing instance.
    /// `&self` borrows the instance to read coordinates.
    fn distance(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    // Call associated function on the type name.
    // This creates the first instance.
    let p = Point::new(3.0, 4.0);

    // Call method on the instance variable.
    // Rust auto-refs this to `Point::distance(&p)`.
    let d = p.distance();
}

Notice the call sites. Point::new uses the type name because no Point exists yet. p.distance uses the variable because the method needs the data inside p. The compiler rejects swapping these. You cannot call p.new() because new is not a method. You cannot call Point::distance() because distance requires an instance.

What happens under the hood

When you write p.distance(), Rust does more than just call a function. It looks at the method signature and sees &self. It automatically inserts a borrow, translating your call to Point::distance(&p). This auto-ref behavior is why methods feel ergonomic. You don't have to manually borrow the instance every time.

Rust tries three resolutions in order when you use dot notation. First, it tries to pass the value by value. If the method takes self, this works and consumes the instance. If that fails, it tries &p. If the method takes &self, this works. If that fails, it tries &mut p. This means you can call a method that takes &self on a mutable variable without adding &. The compiler inserts the borrow for you. This flexibility exists only for methods. Associated functions never get auto-ref. You must provide the arguments exactly as declared.

Real code: mixing construction and behavior

Real code mixes both patterns. You often need associated functions to construct complex state, and methods to mutate or read that state. Consider a Buffer type that manages a byte array. You might want to create a buffer with a specific capacity to avoid reallocations. That's a construction detail, so it belongs in an associated function. Once the buffer exists, you push data or read it. Those actions require the buffer instance, so they are methods.

struct Buffer {
    data: Vec<u8>,
}

impl Buffer {
    /// Creates a buffer with optional initial capacity.
    /// Associated function: no instance needed for setup.
    fn with_capacity(cap: usize) -> Buffer {
        Buffer {
            data: Vec::with_capacity(cap),
        }
    }

    /// Appends bytes to the buffer.
    /// Method: mutates the instance.
    fn push(&mut self, byte: u8) {
        self.data.push(byte);
    }

    /// Returns a slice of the current data.
    /// Method: reads the instance without taking ownership.
    fn as_slice(&self) -> &[u8] {
        &self.data
    }
}

fn main() {
    // Construct with associated function.
    let mut buf = Buffer::with_capacity(1024);

    // Mutate with method.
    buf.push(65);

    // Read with method.
    let slice = buf.as_slice();
}

The API tells a story. with_capacity sets up the object. push and as_slice interact with the object. The separation makes it obvious which functions create state and which functions use state.

Traits and the hidden associated functions

Traits blur the line slightly. A trait can define associated functions. From::from is the classic example. It's an associated function because it takes the source type and returns the target type, not self. You call it as String::from("hello"). The trait system extends the type, but the rule holds: no self means associated function, even inside a trait.

Another common example is Default::default(). This is an associated function that creates a default instance. You call it as Vec::new() or Vec::default(). Both work, but new is an associated function on Vec, and default is an associated function on the Default trait. The community convention is to prefer new for constructors when available, and default when you want the zero-value semantics.

Pitfalls and compiler errors

The compiler catches mix-ups immediately. If you try to call an associated function on an instance, Rust looks for a method with that name and fails. You'll get E0599 (no function or associated item named X found for struct Y). The fix is switching to double colon syntax.

If you try to call a method on the type name, Rust sees the function signature requires self but you provided nothing. You'll get E0061 (this function takes 1 argument but 0 arguments were provided). The compiler is telling you the function expects an instance. Pass one, or switch to dot notation on an instance.

A subtle trap involves consuming methods. If a method takes self by value, it consumes the instance. If you call it on a variable, that variable becomes unusable afterward. This is correct behavior, but it can surprise developers coming from languages where methods always borrow. The compiler warns you with E0382 (use of moved value) if you try to use the variable after a consuming method call. Trust the error code. E0061 means you forgot the instance. E0599 means you used the wrong syntax.

Decision: methods vs. associated functions

Use associated functions for constructors when you need to create an instance and no object exists yet. Use associated functions for type-level utilities when the operation depends on constants or external inputs rather than instance fields. Use methods when the operation must read or mutate the instance's internal state. Use methods for public API ergonomics when dot notation improves readability and auto-ref reduces boilerplate. Use associated functions inside traits when you are defining conversion logic like From::from that transforms external data into the implementing type.

The community treats new as the default constructor name. If construction can fail, switch to try_new or from. Stick to these names. Readers recognize them instantly. Deviating makes your API feel foreign.

If it needs self, it's a method. If it builds self, it's an associated function. The compiler enforces this distinction, so let it guide your design.

Where to go next