Writing JavaScript You Won’t Hate Reading Six Months Later
8 mins read

Writing JavaScript You Won’t Hate Reading Six Months Later

I spent last Tuesday staring at a 4,000-line Express controller. It was supposed to be a simple backend for a note-taking app. Instead, it was a nesting doll of callbacks, mutated variables, and deeply nested if statements that made me want to close my laptop and walk into the ocean.

Well, that’s not entirely accurate — people do talk about “clean code” like it’s some philosophical mountain you have to climb. But really, it’s just writing JavaScript so the next person — usually you, heavily caffeinated and under a deadline — can figure out what the hell is going on without running a debugger for three hours.

And over the last few years, I’ve completely changed how I write JS. I stopped caring about clever one-liners. I started caring about predictability. Here is what actually works in practice.

Functions: Stop the Parameter Madness

I used to write functions that took five or six arguments. Then I’d call them and completely forget the order. Was it (user, id, token) or (id, user, token)?

Just use object destructuring. Always. If a function takes more than two arguments, it should take an object. This gives you named parameters by default and makes your code self-documenting.

Node.js logo - Node.js Logo PNG Vector (SVG) Free Download
Node.js logo – Node.js Logo PNG Vector (SVG) Free Download
// The old way: I will 100% mess up this order
function createNote(title, content, authorId, isPublished, tags) {
  // ...
}
createNote("My Note", "Hello world", "user_99", true, ["js"]);

// The sane way
function createNote({ title, content, authorId, isPublished = false, tags = [] }) {
  // You can set defaults right in the signature.
  // The order doesn't matter anymore.
}

createNote({
  authorId: "user_99",
  title: "My Note",
  content: "Hello world",
  tags: ["js"],
  isPublished: true
});

This probably saved my ass on a project last month when we had to add an optional workspaceId to 40 different API calls. I just added it to the destructured object. No breaking changes. No hunting down every function call to append a null value.

APIs and Async: Escaping Try/Catch Hell

If your API route handlers are 80% try/catch blocks, you’re burying the actual logic under error-handling boilerplate.

But if you’re stuck on an older codebase or writing generic async utilities, you need a wrapper. And I got so tired of typing the exact same error logic that I use this higher-order function everywhere.

// A simple wrapper to catch async errors and pass them to Express 'next'
const asyncHandler = (fn) => (req, res, next) => {
  return Promise.resolve(fn(req, res, next)).catch(next);
};

// Now your API routes actually look like routes
app.post('/api/notes', asyncHandler(async (req, res) => {
  const { title, content } = req.body;
  
  // No try/catch needed. If db.insert fails, the wrapper catches it.
  const newNote = await db.notes.insert({ title, content });
  
  res.status(201).json({ data: newNote });
}));

Keep your API handlers incredibly thin. They should do exactly three things: parse the request, call a service function, and return the response. That’s it. If you have database queries mixed with HTTP header logic, you’re building a trap for yourself.

DOM: Stop Using HTML as a Database

Switching gears to the frontend. The absolute biggest mess I see in vanilla JS or lightweight client-side code is using the DOM to store state.

Node.js logo - Green Grass, Nodejs, JavaScript, React, Mean, AngularJS, Logo ...
Node.js logo – Green Grass, Nodejs, JavaScript, React, Mean, AngularJS, Logo …

Reading .innerHTML or parsing data-* attributes to figure out what your application should do next is a massive anti-pattern. The DOM is a reflection of your state, not the source of truth.

// ❌ TERRIBLE: Reading from the DOM to determine logic
document.querySelector('#save-btn').addEventListener('click', () => {
  const isEditing = document.querySelector('#editor').classList.contains('active');
  const noteId = document.querySelector('#note-title').getAttribute('data-id');
  
  if (isEditing) {
    // do something...
  }
});

// ✅ SANE: Keep state in JS, render to the DOM
const state = {
  isEditing: false,
  currentNoteId: null
};

document.querySelector('#save-btn').addEventListener('click', () => {
  // Logic relies on JS state, completely decoupled from the UI
  if (state.isEditing && state.currentNoteId) {
    saveNote(state.currentNoteId);
  }
});

When you separate your state from your UI, you can test your logic without needing a browser environment. You just test the state object.

The “Clean Code” Performance Trap

And here’s a specific gotcha I ran into recently that completely changed my perspective on abstraction.

frustrated programmer at laptop - Stressed overworked developer programming html code on laptop and ...
frustrated programmer at laptop – Stressed overworked developer programming html code on laptop and …

I got obsessed with making everything pure, declarative functions. We had a background job processing a massive array of analytics events—about 85,000 records. To keep the code “clean,” I chained .map(), .filter(), and .reduce().

Why? Because every time you chain an array method, JavaScript creates a completely new array in memory. For 85,000 records, I was creating and destroying hundreds of thousands of objects in milliseconds.

I rewrote it using a basic, ugly for...of loop.

// It's not as pretty, but it doesn't melt the server
const processedStats = {};

for (const event of massiveEventArray) {
  if (event.type !== 'click') continue; // filter
  
  const normalizedId = event.userId.toLowerCase(); // map
  
  // reduce
  if (!processedStats[normalizedId]) {
    processedStats[normalizedId] = 0;
  }
  processedStats[normalizedId]++;
}

The lesson? Clean code doesn’t mean ignoring how the V8 engine actually works. Sometimes a basic loop is exactly what you need. Don’t sacrifice your server’s RAM just because array chaining looks better in a pull request.

Write code for humans first, absolutely. But when you hit a bottleneck, don’t be afraid to write something a little “ugly” if it gets the job done efficiently. Just leave a comment explaining exactly why you did it.

Node.js Documentation
Node.js GitHub Repository
MDN Web Docs – JavaScript Functions

Questions readers ask

Should JavaScript functions use object destructuring instead of positional arguments?

Yes, any function taking more than two arguments should accept a single object with destructured parameters. This gives you named parameters, lets you set defaults right in the signature, and makes argument order irrelevant. It also means adding a new optional field, like a workspaceId, doesn’t break existing calls — you just add it to the destructured object without hunting down every call site.

How do you avoid try/catch boilerplate in Express async route handlers?

Wrap async handlers in a higher-order function like asyncHandler, defined as (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next). This forwards any rejected promise to Express’s next() error middleware, so individual routes no longer need try/catch blocks. Your route bodies become thin: parse the request, call a service function, and return the response.

Why is storing application state in the DOM considered an anti-pattern?

The DOM should be a reflection of your state, not the source of truth. Reading .innerHTML or parsing data-* attributes to drive logic couples your application behavior to your UI, making it fragile and untestable. Keep state in a plain JavaScript object and render it to the DOM. That way you can test logic against the state object directly, without needing a browser environment.

Why is chaining map filter and reduce slow on large arrays in JavaScript?

Each chained array method creates a brand new array in memory. Processing 85,000 analytics records through .map().filter().reduce() generates hundreds of thousands of intermediate objects in milliseconds, which hammers memory. Rewriting the same logic as a single for…of loop that filters, normalizes, and accumulates into one result object avoids the allocations entirely. It looks uglier but doesn’t melt the server.

Leave a Reply

Your email address will not be published. Required fields are marked *