Skip to content
aipatterns.dev

Code smellDispensables

Dead Code

Variables, parameters, branches, methods or classes that nothing uses still sit in the codebase, costing reading time and misleading readers.

Also known as
Unused code, Unreachable code

Signs

  • The editor or linter greys out a variable, import or private method.
  • A function has no callers in the repository.
  • A branch can never run: a condition on a feature flag removed a year ago, or a case for an enum value that no longer exists.
  • A parameter is passed by every caller and ignored by the function.
  • Large blocks are commented out "in case we need it".

Why it happens

Requirements change and the old path is left in place because removing it feels risky. Feature flags are rolled out to 100% and never cleaned up. Refactorings leave orphaned helpers. Commented-out code is kept because people do not trust version control to remember it.

Example

A checkout service still carries a legacy payment path and an unused parameter.

checkout.ts
import { legacyGateway } from "./legacy-gateway"; // only used below
import { stripe } from "./stripe";

const USE_LEGACY_GATEWAY = false; // rolled out in 2023

export async function charge(orderId: string, cents: number, currency: string, retries: number) {
  // const fraud = await fraudCheck(orderId);
  // if (fraud.score > 0.8) throw new Error("fraud");
  if (USE_LEGACY_GATEWAY) {
    return legacyGateway.charge(orderId, cents);
  }
  return stripe.charge({ orderId, amount: cents, currency });
}

function formatLegacyReceipt(orderId: string): string {
  return `LEGACY-${orderId}`;
}

retries is ignored, the flag is constant, the receipt helper has no callers, and the fraud check is a comment. After cleanup:

checkout.ts
import { stripe } from "./stripe";

export async function charge(orderId: string, cents: number, currency: string) {
  return stripe.charge({ orderId, amount: cents, currency });
}

Git history keeps the old code if anyone needs it.

Treatment

  • Delete it. Version control is the archive.
  • If a parameter is unused, use Remove Parameter and update every caller.
  • If a class became unused or nearly empty, delete it, or use Inline Class if a fragment is still needed.
  • If a subclass is no longer instantiated, use Collapse Hierarchy.
  • For flag-guarded code, remove the flag and the losing branch together.
  • Use the compiler, linter (noUnusedLocals, noUnusedParameters), coverage reports and "find references" to locate candidates; confirm before deleting.

Payoff

  • Less code to read, search, compile and keep secure.
  • Readers stop wondering whether a path is live.
  • Upgrades and refactorings no longer have to carry dead dependencies along.

When to ignore

  • The symbol is part of a published library API. Removing it is a breaking change; deprecate it first and follow your versioning policy.
  • It is called by reflection, dependency injection, a framework by name, a scheduled job, a CLI, or another repository. "No references" in one repo does not prove "no callers".
  • The code is behind a flag that is still live in some environment or for some tenant.

Agent checklist

  • You searched for callers by name across the repo, including string references, config, routes, DI registrations and templates.
  • The symbol is not exported from a published package, or you deprecated it instead of deleting it.
  • Flag-guarded code was removed only after confirming the flag's value in every environment.
  • Unused imports and dependencies freed by the deletion were removed too.
  • The build, type check and full test suite pass after deletion.

Treatments

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.