Dead Code and Duplicates: How to Remove Them Safely
What dead code is, why vibe-coded apps collect so much of it, and how to remove it safely using usage search, tooling, logs, feature flags and small commits.
Dead code is code that sits in a project but never runs: unused files, functions nobody calls, pages nothing links to, branches of conditions that never fire. Duplicates are its close relatives: two or three versions of the same function, of which only one is alive, and it isn’t always obvious which.
Projects written with AI collect a lot of it. The assistant happily writes a new function instead of finding the existing one, keeps the previous version “just in case”, and doesn’t clean up after an experiment that didn’t work.
Why it matters if it doesn’t break anything
Dead code doesn’t crash and doesn’t slow anything down, so it’s easy to tolerate. It costs you in other ways:
- you spend time reading and maintaining things that don’t run;
- your AI assistant finds the old version of a function and edits that instead of the live one, the change “doesn’t apply”, and you can’t work out why;
- a library upgrade breaks code nobody calls, and you have to fix it anyway;
- dead code sometimes includes open endpoints nobody has reviewed, and they’re perfectly reachable from outside.
That last point is the real reason to take this seriously. A forgotten API route with no permission check isn’t clutter, it’s a door.
How to find dead code
Usage search. The simplest and most reliable method. For each suspicious function or component, search the whole project for where it’s called. Editors do this with “Find all references”, which is more precise than plain text search.
Tooling. JavaScript and TypeScript have tools that find unused files, exports and dependencies, such as knip. In other languages the compiler or linter often warns about unused functions and variables on its own. Run it and read the list, but don’t delete everything on it: the tool doesn’t know a function is called by an external service or a cron job.
Logs and analytics. Check which pages and endpoints were actually hit over the last month or two. A route that received zero requests is a candidate for removal.
Coverage. If you have tests, a coverage report shows code that no test executes. That’s not proof of death, but it’s a good reason to look.
Git history. A file nobody has touched in a year and nothing references is probably left over from an earlier life of the project.
Where to look for duplicates
Names give duplicates away: formatPrice and formatPrice2, UserCard and UserCardNew, api.ts and api-old.ts. So do similar chunks of code across features, like the same permission check pasted into five handlers.
The order with duplicates: find the live version, confirm where calls go, point the rest at the live one, and only then delete the extras. If the versions differ, work out which difference matters. Sometimes the “old” function does something the new one forgot.
How to delete without fear
Small commits. One removal, one commit, with a clear message. If something breaks, you revert exactly one change and immediately know what was alive.
Switch off first, delete later. If you’re unsure whether a route or feature is used, don’t delete it straight away. Hide it behind a flag, or add a log line like deprecated route hit at the top of the handler. Wait a week or two. If the line never shows up, delete it calmly.
Delete, don’t comment out. Commented-out code is still dead code, and it gets in the way of reading on top of that. Git remembers everything: if you need it back, it’s in the history.
Check after every step. Build, tests, a quick run through the main flows. If there are no tests, at least open the key pages by hand.
Don’t mix it with other changes. Removing dead code and refactoring live code belong in separate commits. Otherwise, when something breaks, you can’t tell which one did it.
How not to pile it up again
Add a rule to your AI assistant’s instructions: search for an existing function before writing a new one, don’t keep old versions, and remove code from failed attempts. Run an unused-code tool once a month; it takes minutes. And review changes before committing: new files ending in 2 or new should raise questions.
In short
- Dead code is code in the project that never runs; duplicates are multiple versions of the same thing.
- It slows reading, confuses the AI and sometimes leaves unreviewed endpoints open.
- Find it with usage search, tools like
knip, logs, coverage and git history. - Switch off or log anything uncertain first, and delete after a quiet week.
- Remove in small, separate commits, and never comment out instead of deleting: git remembers.
SUBXCAT can run this kind of clean-up with your team and set up checks so the clutter doesn’t come back.