JavaScript Code Formatting Best Practices

Learn how to write clean, consistent JavaScript with Prettier, ESLint, and style guides. Covers indentation, naming conventions, and team configuration.

By Belqassim Djafer · Published · Updated

Why Code Formatting Matters

Consistent formatting reduces cognitive load when reading code, eliminates pointless style debates in code reviews, and makes version diffs cleaner. A properly formatted codebase allows developers to focus on logic rather than cosmetics.

Code is read far more often than it is written. Readable, consistently styled code directly impacts productivity and reduces bugs introduced during maintenance.

The Core Tools: Prettier and ESLint

Prettier: An Opinionated Formatter

Prettier formats your code automatically with minimal configuration. It handles indentation, line length, quotes, semicolons, and trailing commas. Its key advantage is eliminating style debates — you accept Prettier's opinions and move on.

npm install --save-dev prettier
// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "printWidth": 100,
  "trailingComma": "es5"
}
npx prettier --write src/

ESLint: A Code Quality Linter

ESLint finds bugs, enforces best practices, and catches common errors. It is complementary to Prettier: ESLint handles code quality, Prettier handles formatting.

npm install --save-dev eslint @eslint/js
// eslint.config.js (flat config, ESLint 9+)
import js from "@eslint/js";

export default [
  js.configs.recommended,
  {
    rules: {
      "no-unused-vars": "warn",
      "no-console": "warn",
      "eqeqeq": "error",
    }
  }
];

Using Both Together

Use eslint-config-prettier to disable ESLint formatting rules that conflict with Prettier:

npm install --save-dev eslint-config-prettier

Indentation: Spaces vs. Tabs

The community has largely converged on 2 spaces for JavaScript/TypeScript. Arguments:

  • 2 spaces: compact, readable at deep nesting levels, universally rendered
  • 4 spaces: easier to distinguish levels, preferred in Python/C# communities
  • Tabs: allows each developer to set their preferred visual width, but inconsistent in web environments

The most important thing is consistency within a project. Use .editorconfig to enforce it:

# .editorconfig
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

Naming Conventions

Identifier Convention Example
Variables camelCase userName, isActive
Functions camelCase getUserById()
Classes PascalCase UserService
Constants SCREAMING_SNAKE MAX_RETRIES
Private fields camelCase with _ prefix _cache
React components PascalCase UserProfile
Files (component) PascalCase UserProfile.tsx
Files (utility) kebab-case string-utils.ts

Modern JavaScript Style Recommendations

Use const by Default, let When Needed

// Good
const maxRetries = 3;
let currentAttempt = 0;

// Avoid: var has function scope, not block scope
var old = "avoid this";

Prefer Arrow Functions for Callbacks

// Good
const doubled = numbers.map(n => n * 2);

// Verbose
const doubled = numbers.map(function(n) { return n * 2; });

Destructuring

// Good: clear, concise
const { name, email } = user;
const [first, ...rest] = items;

// Verbose
const name = user.name;
const email = user.email;

Optional Chaining and Nullish Coalescing

// Safe navigation
const city = user?.address?.city ?? "Unknown";

// Without optional chaining (verbose and error-prone)
const city = user && user.address && user.address.city ? user.address.city : "Unknown";

Template Literals

// Good
const message = `Hello, ${name}! You have ${count} messages.`;

// Concatenation (avoid)
const message = "Hello, " + name + "! You have " + count + " messages.";

Async Code Formatting

Async/Await vs Promises

// Good: async/await is more readable
async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } catch (error) {
    console.error("Failed to fetch user:", error);
    throw error;
  }
}

// Promise chain (harder to read at multiple levels)
function fetchUser(id) {
  return fetch(`/api/users/${id}`)
    .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
    .catch(err => { console.error(err); throw err; });
}

Import Organisation

Group and order imports consistently:

// 1. Node built-ins
import { readFile } from "fs/promises";
import path from "path";

// 2. External packages
import express from "express";
import { z } from "zod";

// 3. Internal aliases / absolute paths
import { db } from "@/lib/database";
import { UserService } from "@/services/user";

// 4. Relative imports
import { formatDate } from "./utils";
import styles from "./Component.module.css";

Use eslint-plugin-import or a Prettier plugin to enforce import order automatically.

Git Hooks: Automate Formatting on Commit

npm install --save-dev husky lint-staged
npx husky init
// package.json
{
  "lint-staged": {
    "*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md,yaml}": ["prettier --write"]
  }
}

With this setup, code is automatically formatted and linted before every commit, ensuring the repository always contains well-formatted code regardless of each developer's editor settings.

Conclusion

A consistent code style enforced by Prettier + ESLint + lint-staged eliminates an entire category of review comments and makes code maintenance easier. Invest 30 minutes setting up these tools at the start of a project — they save hours of back-and-forth over months of development. Use our JavaScript Formatter to quickly format code snippets without setting up a local toolchain.

Related Tools