We often talk about "Clean Code," but we rarely talk about the specific habits that make code "Dirty."
Dirty code isn't necessarily messy formatting—Prettier fixes that. True "dirty code" is an architectural liability. It works on your machine. It passes the unit tests. But it leaks memory, it lies to your monitoring tools, or it creates state-management nightmares that will haunt the next developer (who is likely you, three months from now).
Here are 6 common anti-patterns I see in code reviews, and exactly how to sanitize them.
1. The "Fire-and-Forget" Loop
Context: Node.js / Async Logic
The Smell 🤢
Using forEach with an async callback to process a list.
// ❌ The Dirty Way
async function emailActiveUsers(users: User[]) {
users.forEach(async (user) => {
await sendEmail(user.id);
});
console.log('All emails sent!'); // Lie. This prints immediately.
}
Why it stinks
Array.prototype.forEach is not promise-aware. It fires the callback for every item and immediately moves on.
- Timing Ambiguity: The function finishes before the work does. The
console.logruns while emails are still sending. - Uncontrolled Concurrency: If
usershas 10,000 items, you just opened 10,000 network requests simultaneously. You will crash your mail server or hit rate limits instantly.
The Sanitized Fix 🧼
Use for...of for sequential execution, or Promise.all (with a concurrency limiter like p-map if the list is huge) for parallel execution.
// ✅ The Sanitized Way (Sequential)
async function emailActiveUsers(users: User[]) {
for (const user of users) {
await sendEmail(user.id);
}
console.log('All emails actually sent.');
}
// ✅ The Sanitized Way (Parallel)
async function emailActiveUsers(users: User[]) {
await Promise.all(users.map(user => sendEmail(user.id)));
console.log('All emails actually sent.');
}
2. The "Software-Side" Filter
Context: Backend / Database
The Smell 🤢
Fetching all records from the database and filtering them in JavaScript.
// ❌ The Dirty Way
const products = await db.product.findMany(); // Fetches 50,000 rows
const available = products.filter(p => p.stock > 0 && p.isActive);
Why it stinks
This is the silent killer of API performance.
- Memory Hog: You load 50,000 objects into Node.js RAM just to discard 40,000 of them.
- Network Latency: You transferred megabytes of data over the wire unnecessarily.
- No Indexing: JavaScript filtering is $O(n)$. Database filtering (with indexes) is $O(log n)$.
The Sanitized Fix 🧼
Push the logic down to the database. It is optimized for this; your Node process is not.
// ✅ The Sanitized Way
const available = await db.product.findMany({
where: {
stock: { gt: 0 },
isActive: true
}
});
3. The "State Mirror"
Context: React / Frontend
The Smell 🤢
Creating useEffect chains to sync two pieces of state.
// ❌ The Dirty Way
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Why it stinks
- Double Render: React renders once for the name change, then runs the effect, updates
fullName, and renders again.
The Sanitized Fix 🧼
Derive state during render. If a value can be calculated from existing props or state, it should not be in useState.
// ✅ The Sanitized Way
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
// Calculated on the fly. No extra render. No desync.
const fullName = `${firstName} ${lastName}`;
4. The "Lying" 200 OK
Context: REST API Design
The Smell 🤢
Sending an HTTP 200 status code, but including an error in the body.
// ❌ The Dirty Way
res.status(200).json({
success: false,
error: "Unauthorized access"
});
Why it stinks
- Breaks Monitoring: Your Datadog/NewRelic dashboard sees 100% success rate (all green), while your users are actually facing errors.
- Confuses Clients: Frontend libraries (Axios, TanStack Query) treat 200 as success. You force the frontend dev to write manual checks (
if (!data.success) throw ...) instead of using the library's built-in error handling.
The Sanitized Fix 🧼
Use the HTTP protocol as intended.
// ✅ The Sanitized Way
res.status(401).json({
message: "Unauthorized access"
});
5. The "Redundant Envelope"
Context: API Response Structure
The Smell 🤢
Wrapping every response in a generic object that restates what the HTTP protocol already told us.
// ❌ The Dirty Way
// GET /api/users/1
{
"success": true, // We already know, it was a 200 OK
"code": 200, // We already know, it's in the header
"data": { // The nested nightmare
"id": 1,
"name": "Alice"
}
}
Why it stinks
- Ambiguity: Can I have
success: truebutcode: 500? Can I havesuccess: falsebutdatais populated? This structure allows for impossible states. - The
data.dataNightmare: Frontend developers have to writeresponse.data.datato access the actual content. - Type Guard Pain: In TypeScript, your response type becomes a messy union where
datamight be generic or null depending on the boolean flag.
The Sanitized Fix 🧼
Return the resource, and only the resource.
If I ask for a User, give me a User. Use the Headers for meta-data (like pagination links or auth tokens) and Status Codes for success/failure.
// ✅ The Sanitized Way
// GET /api/users/1 -> Status 200 OK
{
"id": 1,
"name": "Alice"
}
// GET /api/users/1 -> Status 404 Not Found
{
"message": "User not found"
}
6. The "Trust Me" Type Cast
Context: TypeScript
The Smell 🤢
Using as to silence the compiler when handling external data (API inputs, fetch results).
// ❌ The Dirty Way
interface User { id: number; name: string; }
// We assume the API returns exactly this.
// If it returns { "id": "oops" }, our app crashes later.
const user = await fetch('/api/user').then(r => r.json()) as User;
Why it stinks
as User shuts off TypeScript validation. It is you telling the compiler: "I guarantee this is a User, don't check it."
But you cannot guarantee external data. If the API changes, your app explodes at runtime with undefined is not a function, and TypeScript won't save you.
The Sanitized Fix 🧼
Validate data at the runtime boundary using a schema validator like Zod.
import { z } from "zod";
// ✅ The Sanitized Way
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
const response = await fetch('/api/user').then(r => r.json());
// Throws a clear error instantly if data is wrong.
// 'user' is automatically inferred as typed { id: number, name: string }
const user = UserSchema.parse(response);
The Meta-Lesson: Ambiguity is the Enemy
If you look closely at all the smells above—from the "Envelope" pattern to the "State Mirror"—they all share one fatal flaw: Ambiguity.
-
Ambiguous State: "Is
fullNamethe truth, or arefirstName+lastNamethe truth?" -
Ambiguous Status: "The header says 200 OK, but the body says
success: false." - Ambiguous Timing: "The function returned, but the loop is still running."
Great code is Unambiguous. It has one source of truth, one way to signal errors, and one timeline of execution. Remove the ambiguity, and you remove the bugs.
Top comments (0)