Europe/Kyiv
Posts

Magic Numbers & Magic Strings — The Code That Only the Author Understands

April 26, 2026
Magic Numbers & Magic Strings
You're reading someone else's code — or your own from six months ago — and you hit this:
if (user.role === 2 && order.status === 7) {
  applyDiscount(0.15);
}
What is role 2? What does status 7 mean? Why exactly 0.15? You have no idea without digging through the whole codebase or asking the original author. These are magic numbers and magic strings — literal values dropped directly into logic with no explanation of what they represent. They're one of the most widespread code smells, and they sneak in gradually, one harmless-looking constant at a time. The issue isn't just readability. Magic values create a chain of real, practical problems: You can't search for meaning. If 2 means "admin role", that 2 appears in dozens of places. When the requirement changes, you have to find every single one — and hope you didn't miss any. They break silently. If a magic string like "pending" is scattered across 10 files and a backend engineer renames the status to "awaiting", nothing fails at compile time. It just breaks at runtime, in production, on a Friday. They create duplication. The same value gets typed in by different developers in different files. Now you have 0.15 as a discount in four places, and they drift out of sync over time. They fail the "3am test". If you got woken up at 3am by a production bug and saw if (type === 4), would you know what to do? Magic values show up in every layer of a web application:
// ❌ What does any of this mean?
if (user.role === 2) { ... }
if (order.status === 7) { ... }
if (subscription.plan === 'pro_v2_monthly') { ... }

setTimeout(syncData, 86400000);

const fee = amount * 0.029 + 0.30;

if (response.code === 'ERR_INSUFFICIENT_FUNDS') { ... }

const MAX = 3; // three what? retries? attempts? items?
Each of these forces the next developer to stop, context-switch, and go hunting for what the value means. That's cognitive overhead that compounds across a whole team. The simplest fix: give the value a name that explains its intent.
// ✅ The name explains the intent
const ADMIN_ROLE_ID = 2;
const ORDER_STATUS_SHIPPED = 7;
const STRIPE_FIXED_FEE = 0.30;
const STRIPE_PERCENT_FEE = 0.029;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const MAX_RETRY_ATTEMPTS = 3;

// Now the code reads like a sentence
if (user.role === ADMIN_ROLE_ID) { ... }
setTimeout(syncData, ONE_DAY_MS);
const fee = amount * STRIPE_PERCENT_FEE + STRIPE_FIXED_FEE;
Notice ONE_DAY_MS = 24 * 60 * 60 * 1000 — keeping the math visible is better than writing 86400000. The calculation documents itself. When a value is one of a fixed set of options, a named constant isn't enough — you want a type-safe enum or const object. This way TypeScript catches mistakes at compile time, not runtime.
// ✅ TypeScript enum
enum UserRole {
  Guest = 1,
  User = 2,
  Moderator = 3,
  Admin = 4,
}

enum OrderStatus {
  Pending = 1,
  Processing = 2,
  Shipped = 7,
  Delivered = 8,
  Cancelled = 9,
}

// Usage
if (user.role === UserRole.Admin) { ... }
if (order.status === OrderStatus.Shipped) { ... }

// ✅ Or const object — better for tree-shaking in some setups
const PLAN = {
  Free: 'free',
  Pro: 'pro_v2_monthly',
  Enterprise: 'enterprise',
} as const;

type Plan = typeof PLAN[keyof typeof PLAN];

if (subscription.plan === PLAN.Pro) { ... }
Now if you mistype UserRole.Admni, TypeScript tells you immediately. With 2, you're on your own. Some magic values aren't about domain logic — they're about configuration: timeouts, limits, external API identifiers, feature flags. These belong in a dedicated config file, not scattered inline.
// config/constants.ts
export const PAYMENT = {
  STRIPE_PERCENT_FEE: 0.029,
  STRIPE_FIXED_FEE: 0.30,
  MAX_REFUND_DAYS: 30,
} as const;

export const RETRY = {
  MAX_ATTEMPTS: 3,
  DELAY_MS: 1000,
  BACKOFF_MULTIPLIER: 2,
} as const;

export const CACHE = {
  USER_TTL_MS: 5 * 60 * 1000,      // 5 minutes
  SESSION_TTL_MS: 24 * 60 * 60 * 1000, // 24 hours
} as const;

// Anywhere in your app
import { PAYMENT, CACHE } from '@/config/constants';

const fee = amount * PAYMENT.STRIPE_PERCENT_FEE + PAYMENT.STRIPE_FIXED_FEE;
redis.set(key, value, 'PX', CACHE.USER_TTL_MS);
One file. One source of truth. When Stripe changes their fee structure, you change one line. Magic strings are especially dangerous at API boundaries, where a typo compiles fine but breaks at runtime. This is where TypeScript union types shine:
// ❌ A typo here causes a silent runtime bug
async function updateOrderStatus(id: string, status: string) {
  // 'shiped' compiles fine, breaks at runtime
  await orderRepo.update(id, { status });
}

// ✅ TypeScript catches the typo at compile time
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';

async function updateOrderStatus(id: string, status: OrderStatus) {
  // 'shiped' is a compile error now — caught before it ships
  await orderRepo.update(id, { status });
}
Not every literal value is a magic number. Context matters:
// These are fine — the meaning is obvious from context
const isFirst = index === 0;
const isEmpty = items.length === 0;
const half = total / 2;

// These are magic — the meaning is not obvious
if (score >= 850) { ... }        // 850 what? Why 850?
if (attempts > 3) { ... }        // MAX_RETRY_ATTEMPTS = 3
if (role === 4) { ... }          // UserRole.Admin
The test is simple: would a new team member understand this value without asking anyone? If not — name it. Magic values feel fast to write. But every time someone reads that code — including future you — they pay a small tax in confusion. Over a whole team, over months, that tax adds up. The fix takes thirty seconds: move the value out, give it a name, and let the name carry the meaning. Your code starts reading like documentation, your diffs get smaller, and your 3am debugging sessions get shorter.