DEV Community

Cover image for Stop Shipping Your Dev Logs
Abdul Halim
Abdul Halim

Posted on

Stop Shipping Your Dev Logs

As a senior frontend engineer, I've seen my fair share of "console log graveyards". You know the ones production websites where you open the DevTools and see a messy trail of Object { data: "test" } and HERE 1111 leaking everywhere.

It's messy, it can leak metadata, and it looks amateur. But we still need logs to build things quickly! In 2026, we don't just "turn off" logs; we manage them like pros. Here is the exact stack I use to keep my console clean and my sanity intact.

1. The Architectural Solution: The "Proxy" Logger

Instead of calling console.log directly, we create a small wrapper. Think of it as a "smart filter." By using a JavaScript proxy, we can mimic the entire console API without writing every method manually.

The TypeScript Implementation

Using typeof console ensures that your editor gives you full IntelliSense/autocomplete for every method (.log, .table, .group, etc.).

// utils/logger.ts
const isDev = process.env.NODE_ENV === 'development';

/**
 * The 'debug' proxy mirrors the console API perfectly.
 * In development: works normally.
 * In production: returns an empty function (no-op).
 */
export const debug = new Proxy(console, {
  get(target, prop: keyof typeof console) {
    if (isDev) {
      const value = target[prop];
      if (typeof value === 'function') {
        // Bind 'this' to target (console) so it doesn't lose context
        return value.bind(target);
      }
      return value;
    }

    // In production, return an empty function that does nothing
    return () => {};
  }
}) as typeof console;
Enter fullscreen mode Exit fullscreen mode

2. Keeping ESLint and Biome Happy

Now that we have our debug tool, we need to ensure the team uses it instead of raw console.log.

For ESLint: Set your global rule to error on raw console calls.

// eslint.config.js
export default [{ rules: { "no-console": "error" } }];
Enter fullscreen mode Exit fullscreen mode

For Biome: Use an override in biome.json so the linter ignores the noConsole rule only inside your logger utility.

{
  "linter": { "rules": { "suspicious": { "noConsole": "error" } } },
  "overrides": [
    {
      "include": ["src/utils/logger.ts"],
      "linter": { "rules": { "suspicious": { "noConsole": "off" } } }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

3. Real-World Usage: Seeing it in Action

Once set up, your daily workflow remains unchanged. You get all the power of the console without the risk of shipping it to users.

// components/UserDashboard.tsx
import { debug } from '../utils/logger';

export const UserDashboard = () => {
  const loadData = async () => {
    debug.group('Fetching Dashboard'); // Grouping keeps your dev console tidy

    try {
      const data = await fetch('/api/users').then(res => res.json());
      debug.log('Users received:', data.length);
      debug.table(data); // Beautiful table view in development!
    } catch (err) {
      debug.error('Failed to load:', err);
    } finally {
      debug.groupEnd();
    }
  };

  return <button onClick={loadData}>Load Data</button>;
};
Enter fullscreen mode Exit fullscreen mode

4. The "Senior" Finishing Touch: Build-Time Stripping

Even though our proxy is silent in production, the code for those logs is still in your bundle. To be a true professional, you should physically remove them during the build.

For Next.js Projects

Use the built-in compiler in next.config.mjs:

const nextConfig = {
  compiler: {
    // Shreds all console.* calls during the production build
    removeConsole: process.env.NODE_ENV === 'production',
  },
};
export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

For Vite Projects

Tell Vite to "drop" these specific calls during the minification phase:

// vite.config.ts
export default defineConfig({
  esbuild: {
    drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
  },
});
Enter fullscreen mode Exit fullscreen mode

Why This Wins

By combining a proxy logger, strict linting, and compiler stripping, you've achieved the "golden path" of logging:

  • Zero Visual Noise: No more // eslint-disable comments.
  • Full Power: You still get .table(), .group(), and .warn() with full autocomplete.
  • Maximum Security: No sensitive strings leak into your production source code.
  • Optimal Performance: Your production bundle is smaller because the logs are physically deleted.

It's a small architectural change that makes a massive difference in the long-term health of a project. Happy coding!

Top comments (0)