I've reviewed hundreds of JS/TS codebases over 9 years. The same 20 mistakes show up everywhere, from juniors to seniors. Here they are 👇 1. Not enabling TypeScript strict mode. ↳ You're writing JavaScript with extra syntax. Turn it on. Real bugs hide in loose types. 2. Using `any` as an escape hatch. ↳ Every `any` is a hole in your type safety. Use `unknown`. For external payloads, use Zod. 3. Not using discriminated unions. ↳ Optional fields aren't a type system. Discriminated unions narrow types perfectly. 4. Ignoring return types on exported functions. ↳ Let internals infer. Annotate exports, that's your API contract. 5. Catching errors and swallowing them. ↳ `catch (e) { console.log(e) }` isn't handling. Rethrow, recover, or transform. 6. Not handling Promise rejections. ↳ Node.js terminates on unhandled rejections. Handle or propagate, always. 7. Hardcoding dependencies instead of injecting them. ↳ Direct imports make unit tests painful. Pass deps in. Hours of mocking pain saved. 8. Over-engineering with microservices too early. ↳ 3 devs, 500 users, 7 services? Modular monolith first. Split when you have a reason. 9. Writing 100+ line functions. ↳ Too long means too many things. Extract and name the pieces. 10. Putting business logic in controllers. ↳ Handlers parse, delegate, respond. Business rules live in services. 11. Circular dependencies between modules. ↳ Cyclic imports hand the second module `undefined` bindings. Top-level use (`extends`, `new`) crashes; inside methods, it works until someone adds a top-level reference. 12. Importing everything from barrel files. ↳ `index.ts` re-exports break tree-shaking. Import from source files directly. 13. Mutating function parameters. ↳ Mutations cause action-at-a-distance bugs. Return new objects. 14. Not using `readonly`. ↳ Lock what shouldn't change. `Readonly<T>` is shallow. Arrays need `readonly string[]`. 15. Leaking memory with uncleared listeners and timers. ↳ Every `addEventListener` needs a `removeEventListener`. Every `setInterval`, a `clearInterval`. A `connect()`, a `disconnect()`. 16. Never canceling async operations. ↳ Without AbortController: wasted requests, stale data. In React, the #1 cause of old-data bugs. 17. Running independent async ops sequentially. ↳ Use `Promise.all` when they don't depend on each other. Don't turn 3×1s into 3s. 18. Using `Date` for everything. ↳ Same string parses differently on server vs. browser. Use `date-fns` or `Temporal`. 19. Testing for coverage, not for value. ↳ 100% coverage is vanity. Test behavior and edge cases. 20. Not validating input at the boundary. ↳ Validate `req.body` with Zod. Strips unknown fields. Trust types after. How many are in your codebase right now? —— 💾 Save this for later. ♻ Repost to help others find it. ➕ Follow Petar Ivanov + turn on notifications. #javascript
‘any’ is a bug waiting to happen 🔥
Good list. Also, the one pattern I see all the time across a lot of these is that TypeScript gets treated like internal decoration instead of a contract system. 'any', unvalidated 'req.body', missing return types on exported functions, and business logic leaking through controllers are all different ways of saying: “the boundary is not explicit enough.” The tricky part is that the code can still feel typed inside the app, while the actual edges of the system are running on assumptions.
Most engineers reading this list already know most of these. The gap isn't awareness. Teams keep reaching for microservices too early not because they haven't heard the advice, but because org structure or roadmap pressure made the monolith feel riskier to defend than it actually was. Number 9 is the only one on this list that isn't really a developer mistake. It's a leadership mistake dressed in architectural clothing. The other 19 fix with a linter or a PR review. That one takes a conversation no one wants to have.
What stands out is how many of these are about boundaries: inputs, modules, async flows, and ownership of state Petar
“Using any as an escape hatch” should come with a warning label - once it sneaks in, it spreads everywhere.
Great
It's iteresting how many of these are not knowledge gaps. People know them but they just don’t enforce them.
The Async & Performance section hits close to home — memory leaks from uncleared listeners and missing AbortController are two mistakes I've had to debug in real production React/Node codebases. Not using TypeScript strict mode is another one that silently causes so many issues.
I get the frustration with these across codebases. But on point 8, honestly, we've audited teams with 7 services and 500 users, latency spiked 3x from network hops alone. Monolith got them shipping 2x faster. TS strict mode's non-negotiable though.
👋 Join 30,000+ SWEs learning JS, React, Node.js, and Software Architecture: https://thetshaped.dev/