# JavaScript Standards & Style Guide
Principles
- Vanilla first. Use the DOM API, Fetch API, and Web APIs before adding libraries.
- Progressive enhancement. The page works without JS. JS adds behaviour.
- Modules everywhere. Use ES modules (
type="module") for all non-trivial scripts. - No global scope pollution. Nothing should live on
windowintentionally.
File Structure
js/
├── main.js # Entry point — imports and init only
├── modules/
│ ├── dom.js # DOM helpers
│ ├── api.js # Fetch wrappers
│ └── utils.js # Pure utility functions
└── components/
├── modal.js
└── form-validation.js
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Variables | camelCase | userCount, fetchedData |
| Constants | UPPER_SNAKE_CASE | MAX_RETRIES, API_BASE_URL |
| Functions | camelCase, verb-first | fetchUser(), handleSubmit() |
| Classes | PascalCase | ModalDialog, FormValidator |
| Private fields | # prefix (native) | #cache, #isOpen |
| Files | kebab-case | form-validation.js, modal.js |
Variables & Scoping
// Bad — var is function-scoped and hoisted
var count = 0;
// Good — const by default
const MAX_RETRIES = 3;
const user = { name: 'alice' }; // const for object references too
// let only when reassignment is genuinely needed
let attempt = 0;
while (attempt < MAX_RETRIES) {
attempt++;
}
Functions
Prefer named function declarations for top-level functions (hoisting aids readability). Arrow functions for callbacks and inline expressions.
// Top-level: named declaration
function fetchUser(id) {
// ...
}
// Callback: arrow function
const activeUsers = users.filter(user => user.active);
// Event handler: named for debugging (shows up in stack traces)
button.addEventListener('click', handleButtonClick);
function handleButtonClick(event) {
event.preventDefault();
// ...
}
Error Handling
// Always handle promise rejections
async function loadUserData(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`loadUserData failed for id=${id}:`, error);
throw error; // re-throw unless you're handling it here
}
}
// Catch at the call site when you can do something about it
async function renderUserProfile(id) {
try {
const user = await loadUserData(id);
renderProfile(user);
} catch (error) {
showErrorMessage('Could not load profile. Please try again.');
}
}
DOM Interaction
// Query once, reuse reference
const form = document.querySelector('#signup-form');
const submitButton = form.querySelector('[type="submit"]');
// Bad — requerying on each call
function handleInput() {
document.querySelector('#signup-form').classList.add('dirty');
}
// Good — closed over reference
function handleInput() {
form.classList.add('dirty');
}
// Event delegation for dynamic content
document.querySelector('.card-list').addEventListener('click', (event) => {
const card = event.target.closest('.card');
if (!card) return;
handleCardClick(card);
});
// Prefer dataset over custom attributes for JS hooks
// HTML: <button data-action="delete" data-id="42">
const action = event.target.dataset.action;
const id = event.target.dataset.id;
Modules
// utils.js — pure functions, no side effects
export function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
export function debounce(fn, delayMs) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delayMs);
};
}
// api.js — fetch wrappers
const API_BASE_URL = '/api/v1';
export async function get(path) {
const response = await fetch(`${API_BASE_URL}${path}`);
if (!response.ok) {
throw new Error(`GET ${path} failed: ${response.status}`);
}
return response.json();
}
// main.js — entry point, wires everything
import { debounce } from './modules/utils.js';
import { get } from './modules/api.js';
document.addEventListener('DOMContentLoaded', init);
function init() {
const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', debounce(handleSearch, 300));
}
Power of 10 — JavaScript Adaptation
1. Simple Control Flow
No eval(). No new Function(). Avoid deeply nested callbacks — use async/await.
// Bad — callback hell
fetchUser(id, (err, user) => {
if (err) return handleError(err);
fetchOrders(user.id, (err, orders) => {
if (err) return handleError(err);
renderPage(user, orders);
});
});
// Good — async/await, linear flow
async function loadPage(id) {
const user = await fetchUser(id);
const orders = await fetchOrders(user.id);
renderPage(user, orders);
}
2. Fixed Loop Bounds
const MAX_RETRIES = 5;
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
const result = await tryOperation();
if (result.success) break;
if (attempt === MAX_RETRIES - 1) {
throw new Error(`Operation failed after ${MAX_RETRIES} attempts`);
}
}
3. Bounded Data Structures
const MAX_CACHE_SIZE = 200;
class BoundedCache {
#store = new Map();
#maxSize;
constructor(maxSize = MAX_CACHE_SIZE) {
this.#maxSize = maxSize;
}
set(key, value) {
if (this.#store.size >= this.#maxSize) {
// Evict oldest entry (Maps maintain insertion order)
const firstKey = this.#store.keys().next().value;
this.#store.delete(firstKey);
}
this.#store.set(key, value);
}
get(key) {
return this.#store.get(key);
}
}
4. Short Functions
Max ~30 lines for JavaScript functions. Separate DOM manipulation from data logic. A function that fetches, transforms, and renders is three functions.
5. High Assertion via Guard Clauses
function renderUserCard(user) {
if (!user) throw new TypeError('renderUserCard: user is required');
if (typeof user.name !== 'string') throw new TypeError('renderUserCard: user.name must be a string');
if (!user.id) throw new TypeError('renderUserCard: user.id is required');
// logic here
}
6. Minimal Variable Scope
Declare variables in the narrowest block that needs them. Avoid module-level mutable state.
// Bad — outer scope mutation
let result;
if (condition) {
result = computeA();
} else {
result = computeB();
}
// Good — block scoped
const result = condition ? computeA() : computeB();
7. Check All Return Values
// Bad — assuming success
const data = JSON.parse(rawInput);
// Good — guarded
let data;
try {
data = JSON.parse(rawInput);
} catch (error) {
console.error('Failed to parse input:', error);
return null;
}
// Bad — unchecked querySelector
document.querySelector('.submit-btn').addEventListener('click', handler);
// Good — guard against missing element
const submitBtn = document.querySelector('.submit-btn');
if (!submitBtn) {
console.warn('submit button not found — skipping handler');
return;
}
submitBtn.addEventListener('click', handler);
8. Limit Metaprogramming
No eval(), new Function(), or with statements. Avoid Proxy and Reflect outside library code. Use Object.freeze() for truly immutable constants.
// Bad
eval(userProvidedCode);
// Bad — dynamic property access as a dispatch table
function dispatch(action) {
this[action](); // eval-adjacent, unanalyzable
}
// Good — explicit dispatch
const handlers = {
submit: handleSubmit,
reset: handleReset,
cancel: handleCancel,
};
function dispatch(action) {
const handler = handlers[action];
if (!handler) throw new Error(`Unknown action: ${action}`);
handler();
}
9. Limit Data Structure Nesting
Flatten deeply nested objects. If you’re accessing data.response.user.profile.settings.theme, introduce an intermediate variable or refactor the data shape.
// Bad
const theme = config.ui.preferences.display.colors.theme;
// Good — destructure at point of use
const { theme } = config.ui.preferences.display.colors;
// Better — flatten the data structure at the source
const displaySettings = {
theme: 'dark',
fontSize: 14,
contrast: 'high',
};
10. Enable All Static Analysis
// .eslintrc.json
{
"env": { "browser": true, "es2022": true },
"parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
"rules": {
"no-eval": "error",
"no-implied-eval": "error",
"no-new-func": "error",
"no-var": "error",
"prefer-const": "error",
"eqeqeq": ["error", "always"],
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-undef": "error",
"curly": "error",
"no-console": "warn"
}
}
Quick Reference Tables
Go
| Rule | Guidance |
|---|---|
| Simple control flow | No goto; iterate, don’t recurse; depth-guard if needed |
| Fixed loop bounds | for i := range MAX or explicit bound with error at limit |
| Bounded data | Buffered channels with capacity; lru.New[K,V](maxSize) |
| Short functions | ≤50 lines; single responsibility |
| Assertion density | Validate inputs; check postconditions; use errors.Is/As |
| Minimal scope | := in narrowest block; no package-level mutable state |
| Check returns | Never _ an error without a comment; always check ok |
| Limit metaprogramming | Avoid reflect/unsafe; minimal any usage |
| Limit nesting | ≤3 levels; structs over nested maps |
| Static analysis | golangci-lint run; go test -race ./... in CI |
JavaScript / HTML / CSS
| Rule | Guidance |
|---|---|
| Simple control flow | async/await over callbacks; no eval/new Function |
| Fixed loop bounds | for with explicit count; MAX_RETRIES constant |
| Bounded data | BoundedCache pattern; maxSize on all collections |
| Short functions | ≤30 lines; separate fetch / transform / render |
| Assertion density | Guard clauses at function entry; check querySelector results |
| Minimal scope | const default; let only for reassignment; no var |
| Check returns | Guard every querySelector; wrap JSON.parse in try/catch |
| Limit metaprogramming | No eval; explicit dispatch tables over dynamic dispatch |
| Limit nesting | ≤3 levels; destructure at use site |
| Static analysis | ESLint strict config; lint as CI gate |
API Standards
- APIs must be versioned
- Use consistent error response structures
- Prefer idempotent operations where possible
- Return structured errors with machine-readable codes
- Validate request schemas strictly
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User does not exist"
}
}