Stop Using Magic Numbers: A Rust Constants Guide

C…
crusty.rustacean
Published on October 24, 2025 • Updated October 26, 2025

Magic numbers make code impossible to maintain. Learn how Rust constants solve this problem in 5 minutes with practical examples you'll actually use.

best-practices code-quality rust constants
0 likes

pexels-pixabay-41951.jpg

You’ve seen code like this before:

fn calculate_orbit(distance: f64) -> f64 {
    distance * 299792458.0 / 86400.0
}

What do those numbers mean? Why 299792458? What’s 86400?

If you’re the person who wrote this yesterday, you might remember. Come back in three months? Good luck.

In the next 5 minutes, you’ll learn:

  • Why “magic numbers” destroy code maintainability
  • How Rust constants solve this problem elegantly
  • When to use constants vs variables
  • Naming conventions that make your code self-documenting

This isn’t theory. After reading this, you’ll write clearer code that your future self (and teammates) will thank you for.

The Magic Number Problem

Magic numbers are hardcoded values scattered throughout your code with no explanation of what they represent. They’re called “magic” because you have to magically divine their meaning from context.

Why Magic Numbers Are Dangerous

Example: The $125 Million Mars Climate Orbiter

In 1999, NASA lost a $125 million Mars orbiter because one team used metric units and another used imperial. The code was full of hardcoded conversion factors with no indication of which system they represented.

In your code, magic numbers cause:

  • Maintenance nightmares - Change a value in one place, miss it in another
  • Bugs - Type 86400 instead of 86400.0, break everything
  • Cognitive overload - Every reader has to figure out what the number means
  • Impossible refactoring - Which 60 means “seconds per minute” vs “default timeout”?

Enter Constants: Named Values That Don’t Change

A constant in Rust is a value bound to a name that cannot change. It’s declared once, used everywhere, and the name documents what it represents.

Declaring Your First Constant

Here’s how to declare the speed of light as a constant:

const SPEED_OF_LIGHT: u32 = 299_792_458;

Breaking it down:

  • const - The keyword that declares a constant
  • SPEED_OF_LIGHT - The name (convention: SCREAMING_SNAKE_CASE)
  • : u32 - Type annotation (required for constants)
  • = 299_792_458 - The value (note: underscores for readability)

Now instead of this cryptic mess:

fn time_to_destination(distance: f64) -> f64 {
    distance / 299792458.0
}

You write this self-documenting code:

const SPEED_OF_LIGHT: f64 = 299_792_458.0; // meters per second

fn time_to_destination(distance: f64) -> f64 {
    distance / SPEED_OF_LIGHT
}

The benefit: Anyone reading this code immediately understands what’s happening.

Practical Examples You’ll Actually Use

Example 1: Configuration Values

Bad: Magic numbers everywhere

fn setup_server() {
    let timeout = 30;
    let max_connections = 100;
    let retry_attempts = 3;
    
    // 50 lines later...
    if attempts > 3 {  // Wait, is this related to retry_attempts?
        return Err("Too many attempts");
    }
}

Good: Constants at the top

const SERVER_TIMEOUT_SECONDS: u32 = 30;
const MAX_CONCURRENT_CONNECTIONS: u32 = 100;
const MAX_RETRY_ATTEMPTS: u32 = 3;

fn setup_server() {
    let timeout = SERVER_TIMEOUT_SECONDS;
    let max_connections = MAX_CONCURRENT_CONNECTIONS;
    let retry_attempts = MAX_RETRY_ATTEMPTS;
    
    // 50 lines later...
    if attempts > MAX_RETRY_ATTEMPTS {
        return Err("Too many attempts");
    }
}

Why it’s better:

  • Change the timeout once, it updates everywhere
  • No questions about what the numbers mean
  • Easy to find all config values (they’re at the top)

Example 2: Mathematical Constants

Bad: Calculating circle area

fn circle_area(radius: f64) -> f64 {
    3.14159 * radius * radius  // Is this precise enough?
}

Good: Use standard constants

use std::f64::consts::PI;

fn circle_area(radius: f64) -> f64 {
    PI * radius * radius  // Full precision, clear intent
}

Bonus: Rust’s standard library already provides mathematical constants like PI, E, TAU. Use them instead of hardcoding approximations.

Example 3: Application-Wide Settings

const APP_NAME: &str = "TaskMaster Pro";
const VERSION: &str = "2.1.0";
const DEFAULT_THEME: &str = "dark";
const MAX_FILE_SIZE_MB: u32 = 50;

fn display_about() {
    println!("{} v{}", APP_NAME, VERSION);
}

fn validate_upload(size: u32) -> Result<(), String> {
    if size > MAX_FILE_SIZE_MB {
        Err(format!("File exceeds {} MB limit", MAX_FILE_SIZE_MB))
    } else {
        Ok(())
    }
}

The payoff: When you need to update the version or file size limit, you change it in one place.

Constants vs Variables: When to Use Which

Use constants when:

  • ✅ The value never changes (PI, speed of light)
  • ✅ It’s a configuration value used in multiple places
  • ✅ You want to document what a magic number means
  • ✅ The value is known at compile time

Use variables when:

  • ✅ The value changes during program execution
  • ✅ The value comes from user input
  • ✅ The value is computed at runtime
  • ✅ You need mutability

Example: Constants vs Variables

// Constants - known at compile time, never change
const MAX_USERS: u32 = 1000;
const API_ENDPOINT: &str = "https://api.example.com";

fn main() {
    // Variables - computed at runtime, can change
    let current_users = fetch_user_count();
    let mut login_attempts = 0;
    
    // This won't compile - constants are always immutable
    // MAX_USERS = 2000;  ❌ Error!
}

Scoping: Where Can You Declare Constants?

Constants can live anywhere:

Global scope (available everywhere):

const SPEED_OF_LIGHT: f64 = 299_792_458.0;

fn main() {
    println!("{}", SPEED_OF_LIGHT);
}

fn physics_calculation() {
    let result = SPEED_OF_LIGHT * 2.0;
}

Function scope (local to that function):

fn calculate_discount() -> f64 {
    const DISCOUNT_RATE: f64 = 0.15;  // Only available in this function
    
    let price = 100.0;
    price * (1.0 - DISCOUNT_RATE)
}

Module scope (available in that module):

mod config {
    pub const DATABASE_URL: &str = "postgres://localhost/mydb";
    const INTERNAL_TIMEOUT: u32 = 30;  // Private to this module
}

Best practice: Declare constants in the widest scope where they’re needed, but no wider.

Naming Conventions: Making Constants Readable

Rust conventions for constants:

Format: SCREAMING_SNAKE_CASE

Good names:

const MAX_RETRY_ATTEMPTS: u32 = 3;
const DEFAULT_TIMEOUT_MS: u64 = 5000;
const PI: f64 = 3.141592653589793;
const API_BASE_URL: &str = "https://api.example.com";

Avoid:

const x: u32 = 100;           // ❌ Not descriptive
const MaxRetry: u32 = 3;      // ❌ Wrong case convention
const max_retry: u32 = 3;     // ❌ Looks like a variable
const TIMEOUT: u32 = 5000;    // ⚠️  Missing units (seconds? milliseconds?)

Pro tip: Include units in the name when relevant:

  • TIMEOUT_SECONDS not just TIMEOUT
  • MAX_FILE_SIZE_MB not just MAX_FILE_SIZE
  • REFRESH_RATE_HZ not just REFRESH_RATE

Type Annotations: Required, Not Optional

Unlike variables, constants must have explicit type annotations:

// ✅ Correct - type annotation present
const MAX_CONNECTIONS: u32 = 100;

// ❌ Error - type annotation required
const MAX_CONNECTIONS = 100;

Why? Constants must be evaluated at compile time. Explicit types help the compiler ensure the value is valid.

Common Pitfalls to Avoid

Pitfall 1: Trying to Make Constants Mutable

const mut MAX_USERS: u32 = 100;  // ❌ Won't compile

Fix: Constants are always immutable. If you need mutability, use a variable:

let mut max_users: u32 = 100;  // ✅ This works

Pitfall 2: Runtime-Computed Values

const CURRENT_TIME: u64 = SystemTime::now();  // ❌ Won't compile

Fix: Constants must be known at compile time. Use a variable for runtime values:

let current_time: SystemTime = SystemTime::now();  // ✅ This works

Pitfall 3: Forgetting Type Annotations

const PI = 3.14159;  // ❌ Won't compile

Fix: Always include the type:

const PI: f64 = 3.14159;  // ✅ This works

Real-World Example: Refactoring Magic Numbers

Before: Configuration scattered throughout

fn setup_game() {
    let width = 800;
    let height = 600;
    
    // 100 lines later...
    if player.x > 800 {  // Is this the same 800?
        player.x = 0;
    }
    
    // 50 lines later...
    draw_boundary(800, 600);  // Hope these match!
}

After: Constants make intent clear

const WINDOW_WIDTH: u32 = 800;
const WINDOW_HEIGHT: u32 = 600;
const FRAME_RATE: u32 = 60;
const PLAYER_SPEED: f32 = 5.0;

fn setup_game() {
    let width = WINDOW_WIDTH;
    let height = WINDOW_HEIGHT;
    
    // 100 lines later...
    if player.x > WINDOW_WIDTH as f32 {
        player.x = 0.0;
    }
    
    // 50 lines later...
    draw_boundary(WINDOW_WIDTH, WINDOW_HEIGHT);
}

fn update_player(player: &mut Player, dt: f32) {
    player.x += PLAYER_SPEED * dt;
}

Benefits:

  • Change window size once, everything updates
  • Clear what each number represents
  • Easy to tweak game feel (adjust PLAYER_SPEED)
  • No risk of mismatched values

What You Just Learned

In 5 minutes, you’ve learned:

Why magic numbers are dangerous (maintainability, bugs, confusion)
How to declare constants (const NAME: Type = value)
When to use constants vs variables (compile-time vs runtime)
Naming conventions (SCREAMING_SNAKE_CASE with units)
Common pitfalls (mutability, runtime values, missing types)

The core principle: If you write a number that has meaning, give it a name.

Your Action Item

Open your current Rust project right now and find three magic numbers. Turn them into well-named constants.

Example refactoring:

// Before
if input.len() > 50 {
    return Err("Input too long");
}

// After
const MAX_INPUT_LENGTH: usize = 50;

if input.len() > MAX_INPUT_LENGTH {
    return Err("Input too long");
}

That’s it. Three magic numbers → three constants. Takes 2 minutes. Makes your code instantly clearer.

Where to Go From Here

Now that you understand constants, you’re ready for:

Next topic: Pouring the Footings - Learn how Rust’s variable system prevents bugs

Related concepts:

Deeper dive:

The Bottom Line

Magic numbers are lazy. They save you 10 seconds of typing today and cost you 10 minutes of confusion tomorrow (or 10 hours when hunting bugs).

Constants are professional. They document your code, prevent errors, and make maintenance trivial.

The best time to add constants? When you write the code.
The second best time? Right now.

Go refactor those three magic numbers. Future you will thank present you.

C…
crusty.rustacean

Comments

Loading comments...