10 Essential JavaScript Best Practices for Modern Developers

It’s 4:12 AM, the load balancer is screaming, and I’ve just found a recursive proxy trap in the auth module.

My eyes feel like they’ve been rubbed with sandpaper. This is the third night in a row I’ve been awake, staring at a flickering terminal while the rest of the engineering team sleeps off their “sprint success” happy hour. Project Icarus—a name that was supposed to be aspirational but turned out to be a prophecy—is currently a smoking crater in the US-East-1 region.

Why? Because a group of junior developers decided that “Clean Code” meant abstracting every single line of logic into a multi-layered, decorator-heavy, proxy-wrapped nightmare. They didn’t build a service; they built a Rube Goldberg machine out of TypeScript and hubris.

THE INCIDENT: A Symphony of Failure

The first alert hit at 01:15. Latency on the /api/v1/authorize endpoint spiked from 40ms to 12,000ms. Then the memory usage on the Node.js v20.11.0 containers started climbing. It wasn’t a leak; it was a flood.

# PM2 Process Monitor (v5.3.1)
# Project: Icarus-Auth-Service
# Status: CRITICAL

[0] Icarus-Auth-Service | CPU: 98% | Mem: 1.8 GB (Limit 2GB) | Uptime: 4m
[0] Icarus-Auth-Service | FATAL: [worker 1] Potential infinite recursion detected in Proxy handler.
[0] Icarus-Auth-Service | FATAL: [worker 2] CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

<--- Last few GCs --->
[1420:0x684c000]    12402 ms: Mark-sweep 1982.4 (2048.0) -> 1980.1 (2048.0) MB, 412.4 / 0.0 ms  (average mu = 0.142, current mu = 0.008) allocation failure; GC in old space requested
[1420:0x684c000]    12910 ms: Mark-sweep 1980.1 (2048.0) -> 1980.1 (2048.0) MB, 508.2 / 0.0 ms  (average mu = 0.076, current mu = 0.002) allocation failure; GC in old space requested

# Stack Trace:
RangeError: Maximum call stack size exceeded
    at get (auth.middleware.ts:42:21)
    at get (auth.middleware.ts:45:25)
    at get (auth.middleware.ts:45:25)
    ... (repeated 4,000 times)

The logs are a graveyard. Every time a request hit the auth module, the “clever” proxy logic attempted to intercept property access for “security auditing,” which triggered another property access, which triggered another proxy, until the V8 engine gave up on life.

THE AUTOPSY: Architectural Failures of the “Clever” Mind

When I finally dug into the source code, I found a crime scene. The team had used TypeScript 5.3 features not to provide type safety, but to create a DSL (Domain Specific Language) so convoluted that even the compiler was confused.

The core of the failure was a fundamental misunderstanding of how Node.js handles the event loop and memory. They thought they were being “decoupled.” They thought they were following “javascript best” practices by avoiding direct object access. In reality, they were just adding overhead until the system collapsed under its own weight.

H2: Evidence Exhibit A: The Proxy Pattern Abuse

The “Senior” Junior on the team decided that every user object should be wrapped in a recursive Proxy to “ensure data integrity.” This is what happens when you read a blog post about advanced patterns but have never actually had to maintain a production system under load.

Before (Broken):

// The "Clever" way that killed us
export const createSecureUser = (user: User) => {
  return new Proxy(user, {
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);
      if (typeof value === 'object' && value !== null) {
        // Recursive proxying - every nested access creates a new Proxy
        // This is a memory bomb.
        return createSecureUser(value); 
      }
      console.log(`Accessing ${String(prop)}`);
      return value;
    }
  });
};

This code is a disaster. Every time you access user.profile.settings.theme, you aren’t just getting a string. You are creating three new Proxy objects. In a high-concurrency environment with thousands of requests per second, the garbage collector can’t keep up. This isn’t “clean code”; it’s a resource leak disguised as an abstraction.

After (Fixed):
If you want to follow javascript best practices, you use simple, flat data structures and explicit validation. If you need to audit access, you do it at the service layer, not by hijacking the language’s fundamental object behavior.

// The boring, stable way that actually works
export interface User {
  readonly id: string;
  readonly profile: {
    readonly theme: string;
  };
}

// Use a simple utility or a decorator at the controller level
export function logAccess(userId: string, property: string) {
  // Audit logic here - decoupled from the object itself
  process.stdout.write(`User ${userId} accessed ${property}\n`);
}

// Access data directly. It's what the engine is optimized for.
const theme = user.profile.theme;
logAccess(user.id, 'profile.theme');

H2: Evidence Exhibit B: The Barrel Export Bottleneck

I looked at the import statements. They were importing single constants from a shared folder that contained an index.ts file—a “barrel export.” This barrel file exported every single utility, component, and constant in the entire project.

Because of how Node.js v20.11.0 resolves modules, importing one single string from that barrel file forced the engine to load, parse, and execute every single file in that directory. Our cold start time was 45 seconds.

Before (Broken):

// src/constants/index.ts
export * from './auth.constants';
export * from './db.constants';
export * from './api.constants';
export * from './ui.constants'; // Why is UI code in the backend?
// ... 50 more exports

After (Fixed):
Stop using barrel exports. Import exactly what you need from the specific file. It’s not “cleaner” to have a single import line if it costs you 200MB of unnecessary memory overhead.

// Direct imports are faster and allow for better tree-shaking
import { AUTH_TIMEOUT } from './constants/auth.constants';
import { DB_RETRY_LIMIT } from './constants/db.constants';

H2: Evidence Exhibit C: The Async Loop Catastrophe

The “Icarus” project had to migrate some data. The developers used .map() with async/await to process 10,000 records. They thought they were being efficient. Instead, they opened 10,000 simultaneous database connections and crashed the RDS instance.

# Database Logs
# Error: FATAL: remaining connection slots are reserved for non-replication superuser connections
# Error: ConnectionTimeoutError: ResourceRequest timed out

Before (Broken):

// The "I'm sure the DB can handle it" approach
const updateUsers = async (users: User[]) => {
  // This fires all 10,000 promises simultaneously
  await Promise.all(users.map(async (user) => {
    const data = await fetchExternalData(user.id);
    return db.user.update({ where: { id: user.id }, data });
  }));
};

After (Fixed):
Following javascript best practices means respecting your infrastructure. You need concurrency control. You don’t just throw everything at the wall and hope the wall doesn’t break.

import pLimit from 'p-limit';

const updateUsers = async (users: User[]) => {
  // Limit concurrency to 10 simultaneous operations
  const limit = pLimit(10);

  const tasks = users.map((user) => {
    return limit(async () => {
      const data = await fetchExternalData(user.id);
      return db.user.update({ where: { id: user.id }, data });
    });
  });

  await Promise.all(tasks);
};

H2: Evidence Exhibit D: The “Any-script” Plague

We are using TypeScript 5.3, yet the codebase was littered with any. When I asked why, the response was: “The types were getting too complex.”

The types were complex because the architecture was garbage. Instead of fixing the architecture, they bypassed the compiler. This led to a production error where a null value was passed into a function that expected a string, causing a runtime crash that the “clever” error handler caught and then… did nothing with.

Before (Broken):

// Why even use TypeScript?
async function processOrder(order: any) {
  try {
    const price = order.items[0].price; // Crashes if items is empty
    await paymentGateway.charge(price);
  } catch (e) {
    // The "Silent Killer" pattern
    console.error("Error processing order"); 
  }
}

After (Fixed):
Use the type system. If the types are too hard to write, your functions are doing too much. Use ES2023 features like Array.prototype.toSorted() or with() if you need immutability, but keep your types strict.

interface OrderItem {
  price: number;
}

interface Order {
  items: OrderItem[];
}

async function processOrder(order: Order): Promise<void> {
  const firstItem = order.items[0];

  if (!firstItem) {
    throw new Error('Order must contain at least one item');
  }

  try {
    await paymentGateway.charge(firstItem.price);
  } catch (error) {
    // Proper error reporting, not just a console log
    logger.error({ error, orderId: order.id }, "Payment gateway failure");
    throw new PaymentError("Failed to process payment", { cause: error });
  }
}

H2: Evidence Exhibit E: Dependency Bloat and the NPM Black Hole

I ran an npm audit. I wish I hadn’t. The project had 1,400 dependencies for a service that basically just checks a JWT and queries a Postgres DB. They had a library for everything. A library to pad strings. A library to check if a number is even. A library to wrap other libraries.

# npm audit report

Project Icarus
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
High            | 42 vulnerabilities
Critical        | 12 vulnerabilities
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total dependencies: 1,422
Direct dependencies: 84

Every dependency is a liability. Every dependency is code you didn’t write, don’t understand, and have to maintain. The “clever” developer thinks adding a package is “efficient.” The senior architect knows it’s a debt contract signed in blood.

H2: Evidence Exhibit F: Silent Failures in Error Handling

The most infuriating part of this 72-hour hell-climb was the “Global Error Handler.” It was a 500-line monstrosity that used reflection to “categorize” errors. If an error didn’t fit a category, it was swallowed.

Before (Broken):

// The "I'm too smart for simple catch blocks" handler
process.on('unhandledRejection', (reason) => {
  // We'll just log it and hope for the best
  SmartErrorCategorizer.analyze(reason).then(report => {
    if (report.severity === 'LOW') return; // Silent failure
    sendToSentry(report);
  });
});

After (Fixed):
When the process is in an unstable state, javascript best practices dictate that you crash. Let the orchestrator (Kubernetes, PM2) restart the process. Don’t try to be “smart” and keep a zombie process alive.

process.on('unhandledRejection', (reason) => {
  console.error('FATAL: Unhandled Rejection', reason);
  // Give the logger time to flush, then exit
  setTimeout(() => {
    process.exit(1);
  }, 500);
});

THE MANIFESTO: Simplicity is a Feature, Not a Lack of Ambition

I’m looking at the clock. It’s 5:45 AM. The sun is coming up, and I’ve finally stripped out the recursive proxies and replaced them with simple validation logic. The memory usage has stabilized at 150MB. The latency is back down to 30ms.

We’ve been conditioned to believe that complexity equals quality. We think that if a solution is easy to understand, it must be “junior-level.” We chase abstractions like they’re holy relics.

Here is the truth, and I want every developer who worked on Project Icarus to tattoo this on their forearms: Code is a liability.

Every line of code you write is something that can break, something that needs to be tested, and something that the next person (probably a sleep-deprived version of me) has to debug at 4 AM. “Clever” code is a ego trip. It’s a way for developers to prove how much they know about the internals of the language while ignoring the actual requirements of the business.

The Rules of the Graveyard:

  1. Stop Abstracting Problems You Don’t Have Yet. You don’t need a generic repository pattern for a database that will only ever have four tables. You don’t need a multi-provider notification strategy for a system that only sends emails.
  2. The “Clean Code” Book is Not a Bible. If your “clean” code requires five files and three interfaces to add two numbers together, it’s not clean. It’s obstructive.
  3. Performance is a Feature. You cannot “optimize later” if your fundamental architecture is built on top of expensive abstractions like recursive Proxies and massive barrel exports.
  4. TypeScript is a Tool, Not a Game. Use it to prevent bugs, not to create complex type gymnastics that make the code unreadable. If you use any, you’ve failed. If you use unknown and a type guard, you’ve succeeded.
  5. Respect the Platform. Understand the Node.js event loop. Understand how V8 allocates memory. Understand that every await in a loop is a potential bottleneck.

Project Icarus didn’t fail because the developers were bad at coding. It failed because they were too “good” at it. they were so focused on the “how” of the patterns that they forgot the “why” of the software. We don’t build software to create beautiful abstractions; we build software to solve problems.

If your solution creates more problems than it solves, it doesn’t matter how “clean” it is. It’s trash.

I’m going to sleep now. When I wake up, I’m deleting the Proxy module from the codebase. And if I see another barrel export, I’m revoking someone’s git permissions.

Stay simple. Stay boring. Stop being clever. Your production environment depends on it.

Leave a Comment