Three years ago, I made a decision that my senior colleagues thought was career suicide.
I stopped writing ES6 classes in JavaScript.
No more class Foo extends Bar. No more constructor(). No more this.something = something scattered across 40-line boilerplate blocks.
Here's what actually happened.
The Problem With Classes in JS
JavaScript classes are not "real" classes. They're syntactic sugar over prototypal inheritance — a fact the language tries hard to hide from you.
When I was writing class-heavy code, I kept running into the same bugs:
-
thisbehaving unexpectedly inside callbacks - Accidentally mutating shared state on the prototype
- Confusing
super()chains that broke silently - Unit tests that required elaborate mocking just to instantiate a single object
The deeper I went, the more I realized: I wasn't using classes because they were the right tool. I was using them because they felt familiar from Java and Python.
What I Switched To
1. Plain functions and closures
Instead of:
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
}
getCount() {
return this.count;
}
}
I now write:
function createCounter() {
let count = 0;
return {
increment: () => count++,
getCount: () => count,
};
}
No this. No new. No surprises. The state is genuinely private — not just by convention.
2. Pure functions for business logic
Most "methods" in my old classes were just transformations on data. Once I untangled state from behavior, almost everything became a pure function:
// Before
class OrderProcessor {
applyDiscount(order, percent) {
order.total = order.total * (1 - percent / 100);
return order;
}
}
// After
const applyDiscount = (order, percent) => ({
...order,
total: order.total * (1 - percent / 100),
});
Testable in one line. No setup. No teardown.
3. Composition over inheritance
Inheritance hierarchies were my biggest time sink. Deep extends chains mean a change anywhere can ripple unpredictably. I replaced them with simple function composition:
const withLogging = (fn) => (...args) => {
console.log(`Calling with`, args);
return fn(...args);
};
const withValidation = (fn) => (...args) => {
if (!args[0]) throw new Error('First argument required');
return fn(...args);
};
const processOrder = withLogging(withValidation(applyDiscount));
What Actually Changed After 3 Years
✅ My code reviews got faster. Reviewers don't have to trace class hierarchies to understand what a function does.
✅ My tests got simpler. Pure functions don't need mocking frameworks. You pass data in, you get data out.
✅ Onboarding juniors got easier. "It's a function that takes X and returns Y" is always easier to explain than "it's a class that extends this abstract base..."
✅ I stopped fighting this. Seriously. Arrow functions and closures just eliminated a whole category of bugs.
❌ Some things still need classes. React components (historically), custom DOM elements, certain library integrations — classes have a legitimate home. I'm not dogmatic.
The Real Lesson
The goal was never "avoid classes." The goal was to write code that's easy to read, test, and change.
For 90% of my day-to-day JavaScript, functions and plain objects accomplish that better than classes do.
If you're class-heavy right now, I'm not saying throw it all away. But next time you reach for class, ask yourself: would a function and a plain object work just as well here?
More often than not, the answer is yes.
Have you moved away from classes in JavaScript? Or do you think I'm totally wrong? Drop your thoughts in the comments — I read every one.
Top comments (1)
I'm curious how you handled complex state management or