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
86400instead of86400.0, break everything - Cognitive overload - Every reader has to figure out what the number means
- Impossible refactoring - Which
60means “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 constantSPEED_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_SECONDSnot justTIMEOUTMAX_FILE_SIZE_MBnot justMAX_FILE_SIZEREFRESH_RATE_HZnot justREFRESH_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:
- Single Value Data Types - What types can constants be?
- Functions - Using constants in function parameters
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.
Comments