JavaScript Style Guide
Table of Contents
- Naming
- JavaScript Style Rules
- JavaScript Formatting Rules
- Modern JavaScript Features
- Async Programming
- Error Handling
- Performance
- Testing
Naming
Variable and Function Names
Use camelCase for variable and function names (first letter lowercase, subsequent words uppercase).
let totalAmount = 100;
function calculateTotal() {}
Constant Names
Constants should be written in UPPERCASE letters with words separated by underscores. This style clearly distinguishes constants from regular variables, making them easily identifiable.
const MAX_STRING = 100;
Class Names
Class names should follow PascalCase, where each word, including the first, starts with an uppercase letter. This differentiates class names from other identifiers, making them easy to recognize.
class Pelagornis {}
File and Folder Names
For file and folder names, use lowercase letters with hyphens separating words. This format is widely accepted and makes the names easier to read across different systems.
button-component.js
pelagornis-ui/
Method Names
Method names should generally start with a verb to indicate action and should follow camelCase to maintain consistency with variable and function names.
function fetchData() {}
Javascript Style Rules
Semicolons
Always use semicolons at the end of statements. While JavaScript has automatic semicolon insertion, it’s better to include semicolons explicitly to avoid potential issues and ensure consistent behavior.
const x = 5;
const y = 10;
Quotes
For string literals, use single quotes ('
). However, when performing string interpolation, use backticks (```) to create template literals.
const company = "pelagornis";
const greeting = `Hello, ${company}`;
Variable Declarations
Always use const
for values that will not change, and let
for values that can be reassigned. Avoid var
, as it can lead to unexpected behavior due to its function-scoped nature.
const maxCount = 10;
let currentCount = 5;
Comparison Operators
Always use strict equality (===
) and strict inequality (!==
) for comparisons. This ensures that both the value and the type match, preventing issues with JavaScript’s automatic type coercion.
if (x === 5) {
}
if (y !== 10) {
}
Conditional Statements
Always use curly braces around the body of if-else statements, even if there’s only one line of code. This makes the code easier to read and prevents errors when adding more lines in the future.
if (x > 5) {
console.log(`Point: ${x}`);
} else {
// ...
}
switch
Statements
Ensure that a break
statement follows each case
in a switch
statement. This prevents unintended fall-through, where code can accidentally execute multiple case
blocks.
switch (lang) {
case "en":
console.log("English");
break;
case "ko":
console.log("Korea");
break;
default:
console.log("Unknown lang");
}
Loops
Prefer higher-order functions like forEach
, map
, or filter
for iterating over arrays, as these methods are more concise and functional compared to traditional loops.
arr.forEach((item) => console.log(item));
Functions and Methods
Use arrow functions for a more concise syntax, and use named functions rather than anonymous ones for better debugging and easier stack tracing.
const add = (a, b) => a + b;
Asynchronous
For handling asynchronous operations, prefer using async/await
syntax. It is more readable and easier to manage than using then/catch
.
async function fetchData() {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
console.error("Error:", error);
}
}
Objects and Arrays
Whenever possible, use dot notation for accessing properties of objects, as it’s more concise. For manipulating arrays, use higher-order functions like map
, filter
, or reduce
instead of traditional loops.
const user = { name: "Zepa", age: 22 };
console.log(user.name);
Type Conversion
Always use explicit type conversion (e.g., String()
, Number()
) instead of relying on JavaScript’s automatic type coercion, which can lead to confusing or unpredictable results.
const str = String(100);
const num = Number("123");
Javascript Formatting Rules
Indentation
Use 2 spaces for indentation throughout the codebase. This helps keep the code compact while maintaining readability.
Whitespace
Around operators: Always add a space before and after operators (=, +, -, etc.) for clarity.
const sum = a + b;
const product = x * y;
Function parameters: Do not add spaces between the function name and the opening parenthesis.
function sum(a, b) {}
Opening curly braces: Do not add spaces between the function declaration and the opening curly brace.
function hello() {
console.log("Hello");
}
Line Breaks
Break long lines of code to improve readability and maintain a maximum line length of 80–100 characters.
const content =
"VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" +
"LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL";
Blank Lines
Add a blank line at the end of the file to separate it from any subsequent code. Use blank lines between logical blocks of code to increase readability.
function sum(a, b) {}
// Blank line
const
and let
Declaration Order
Declare constants at the top of a block, followed by let
variables. This keeps the code structured and easier to follow.
const MAX_COUNT = 100;
let currentCount = 5;
Comments
Use single-line comments with //
for short explanations, and multi-line comments with /* */
for longer descriptions.
// This function returns the sum of two numbers.
function add(a, b) {
return a + b;
}
/*
* This function returns user details.
* It fetches user information based on the provided ID.
*/
function fetchUser(id) {}
Sorting and Whitespace in Objects
Sort the properties of objects alphabetically for consistency and easier navigation.
const company = {
name: "pelagornis",
location: "Korea",
};
ETC Rules
Modularization and File Separation
Split code into modules, each focusing on a single responsibility. This promotes better organization, testability, and maintainability.
// src/utils/data.js
export function Data(items) {}
JSDoc Usage
Document functions using JSDoc
comments. Provide clear descriptions of parameters, return values, and side effects to help others (and yourself) understand your code.
/**
* Returns the sum of two numbers.
* @param {number} a - The first number
* @param {number} b - The second number
* @returns {number} The sum of a and b
*/
function add(a, b) { return a + b; }
### Modern JavaScript Features
#### Destructuring Assignment
Use destructuring for cleaner variable assignment and function parameters.
```js
// Object destructuring
const user = { name: 'John', age: 30, city: 'Seoul' };
const { name, age, city } = user;
// Array destructuring
const colors = ['red', 'green', 'blue'];
const [primary, secondary, tertiary] = colors;
// Function parameter destructuring
function greetUser({ name, age }) {
return `Hello ${name}, you are ${age} years old`;
}
// Nested destructuring
const config = {
database: {
host: 'localhost',
port: 5432,
credentials: {
username: 'admin',
password: 'secret'
}
}
};
const { database: { host, credentials: { username } } } = config;
Spread and Rest Operators
Use spread and rest operators for array and object manipulation.
// Spread operator for arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
// Spread operator for objects
const user = { name: "John", age: 30 };
const userWithEmail = { ...user, email: "john@example.com" };
// Rest operator for function parameters
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
// Rest operator for destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
Template Literals
Use template literals for string interpolation and multi-line strings.
const name = "John";
const age = 30;
// String interpolation
const greeting = `Hello, my name is ${name} and I am ${age} years old`;
// Multi-line strings
const html = `
<div class="user-card">
<h2>${name}</h2>
<p>Age: ${age}</p>
</div>
`;
// Tagged template literals
function highlight(strings, ...values) {
return strings.reduce((result, string, i) => {
const value = values[i] ? `<mark>${values[i]}</mark>` : "";
return result + string + value;
}, "");
}
const highlighted = highlight`Hello ${name}, you are ${age} years old`;
Optional Chaining and Nullish Coalescing
Use optional chaining and nullish coalescing for safer property access.
// Optional chaining
const user = {
profile: {
settings: {
theme: "dark",
},
},
};
const theme = user?.profile?.settings?.theme;
const email = user?.profile?.email ?? "No email provided";
// Nullish coalescing
const config = {
timeout: null,
retries: undefined,
debug: false,
};
const timeout = config.timeout ?? 5000; // 5000
const retries = config.retries ?? 3; // 3
const debug = config.debug ?? true; // false (not nullish)
Array Methods
Use modern array methods for functional programming.
const users = [
{ id: 1, name: "John", age: 30, active: true },
{ id: 2, name: "Jane", age: 25, active: false },
{ id: 3, name: "Bob", age: 35, active: true },
];
// Filter and map
const activeUserNames = users
.filter((user) => user.active)
.map((user) => user.name);
// Reduce
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
// Find and findIndex
const user = users.find((user) => user.id === 2);
const userIndex = users.findIndex((user) => user.id === 2);
// Some and every
const hasActiveUsers = users.some((user) => user.active);
const allUsersActive = users.every((user) => user.active);
// Flat and flatMap
const nested = [
[1, 2],
[3, 4],
[5, 6],
];
const flattened = nested.flat();
const doubled = nested.flatMap((arr) => arr.map((x) => x * 2));
Async Programming
Promises
Use Promises for handling asynchronous operations.
// Creating promises
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: "John Doe" });
} else {
reject(new Error("Invalid user ID"));
}
}, 1000);
});
}
// Promise chaining
fetchUserData(1)
.then((user) => {
console.log("User:", user);
return fetchUserPosts(user.id);
})
.then((posts) => {
console.log("Posts:", posts);
})
.catch((error) => {
console.error("Error:", error);
});
// Promise.all for parallel execution
Promise.all([fetchUserData(1), fetchUserData(2), fetchUserData(3)])
.then((users) => {
console.log("All users:", users);
})
.catch((error) => {
console.error("Error fetching users:", error);
});
// Promise.allSettled for handling mixed results
Promise.allSettled([
fetchUserData(1),
fetchUserData(-1), // This will fail
fetchUserData(3),
]).then((results) => {
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`User ${index + 1}:`, result.value);
} else {
console.log(`User ${index + 1} failed:`, result.reason);
}
});
});
Async/Await
Use async/await for cleaner asynchronous code.
// Basic async/await
async function fetchUserWithPosts(userId) {
try {
const user = await fetchUserData(userId);
const posts = await fetchUserPosts(user.id);
return { user, posts };
} catch (error) {
console.error("Error:", error);
throw error;
}
}
// Parallel execution with async/await
async function fetchMultipleUsers(userIds) {
try {
const userPromises = userIds.map((id) => fetchUserData(id));
const users = await Promise.all(userPromises);
return users;
} catch (error) {
console.error("Error fetching users:", error);
throw error;
}
}
// Error handling with async/await
async function safeFetchUser(userId) {
try {
const user = await fetchUserData(userId);
return { success: true, data: user };
} catch (error) {
return { success: false, error: error.message };
}
}
Generators
Use generators for creating iterators and handling complex async flows.
// Basic generator
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
// Generator with parameters
function* fibonacci() {
let a = 0,
b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
for (let i = 0; i < 10; i++) {
console.log(fib.next().value);
}
// Async generator
async function* asyncDataGenerator() {
for (let i = 0; i < 5; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
yield `Data ${i}`;
}
}
async function processAsyncData() {
for await (const data of asyncDataGenerator()) {
console.log(data);
}
}
Error Handling
Try-Catch Blocks
Use try-catch blocks for error handling.
// Basic try-catch
function divide(a, b) {
try {
if (b === 0) {
throw new Error("Division by zero is not allowed");
}
return a / b;
} catch (error) {
console.error("Error in divide function:", error.message);
throw error;
}
}
// Try-catch with async/await
async function fetchDataWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error.message);
if (attempt === maxRetries) {
throw new Error(`Failed to fetch data after ${maxRetries} attempts`);
}
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}
Custom Error Classes
Create custom error classes for better error handling.
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "NetworkError";
this.statusCode = statusCode;
}
}
function validateUser(user) {
if (!user.name) {
throw new ValidationError("Name is required", "name");
}
if (!user.email) {
throw new ValidationError("Email is required", "email");
}
return true;
}
function handleUserCreation(user) {
try {
validateUser(user);
// Process user creation
return { success: true, user };
} catch (error) {
if (error instanceof ValidationError) {
return { success: false, error: error.message, field: error.field };
}
throw error;
}
}
Error Boundaries
Implement error boundaries for graceful error handling.
class ErrorBoundary {
constructor() {
this.errors = [];
}
async execute(fn) {
try {
return await fn();
} catch (error) {
this.errors.push(error);
console.error("Error caught by boundary:", error);
return null;
}
}
hasErrors() {
return this.errors.length > 0;
}
getErrors() {
return this.errors;
}
clearErrors() {
this.errors = [];
}
}
// Usage
const boundary = new ErrorBoundary();
const result1 = await boundary.execute(() => fetchUserData(1));
const result2 = await boundary.execute(() => fetchUserData(-1));
if (boundary.hasErrors()) {
console.log("Some operations failed:", boundary.getErrors());
}
Performance
Memory Management
Optimize memory usage and prevent memory leaks.
// WeakMap for private data
const privateData = new WeakMap();
class User {
constructor(name, email) {
privateData.set(this, { name, email });
}
getName() {
return privateData.get(this).name;
}
getEmail() {
return privateData.get(this).email;
}
}
// WeakSet for tracking objects
const processedUsers = new WeakSet();
function processUser(user) {
if (processedUsers.has(user)) {
return; // Already processed
}
// Process user
processedUsers.add(user);
}
// Proper cleanup
class DataManager {
constructor() {
this.listeners = new Set();
this.timers = new Set();
}
addListener(callback) {
this.listeners.add(callback);
}
removeListener(callback) {
this.listeners.delete(callback);
}
startTimer(callback, interval) {
const timer = setInterval(callback, interval);
this.timers.add(timer);
return timer;
}
cleanup() {
this.listeners.clear();
this.timers.forEach((timer) => clearInterval(timer));
this.timers.clear();
}
}
Debouncing and Throttling
Use debouncing and throttling for performance optimization.
// Debounce function
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Throttle function
function throttle(func, limit) {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Usage examples
const debouncedSearch = debounce((query) => {
console.log("Searching for:", query);
}, 300);
const throttledScroll = throttle(() => {
console.log("Scroll event");
}, 100);
// Event listeners
document.getElementById("search").addEventListener("input", (e) => {
debouncedSearch(e.target.value);
});
window.addEventListener("scroll", throttledScroll);
Lazy Loading
Implement lazy loading for better performance.
// Lazy loading with Intersection Observer
class LazyLoader {
constructor() {
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{ threshold: 0.1 }
);
}
observe(element) {
this.observer.observe(element);
}
handleIntersection(entries) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadContent(entry.target);
this.observer.unobserve(entry.target);
}
});
}
loadContent(element) {
const src = element.dataset.src;
if (src) {
element.src = src;
element.classList.add("loaded");
}
}
}
// Usage
const lazyLoader = new LazyLoader();
document.querySelectorAll("img[data-src]").forEach((img) => {
lazyLoader.observe(img);
});
// Lazy loading with dynamic imports
async function loadModule(moduleName) {
try {
const module = await import(`./modules/${moduleName}.js`);
return module;
} catch (error) {
console.error(`Failed to load module ${moduleName}:`, error);
return null;
}
}
// Conditional module loading
async function loadFeature(featureName) {
if (shouldLoadFeature(featureName)) {
const feature = await loadModule(featureName);
if (feature) {
feature.initialize();
}
}
}
Testing
Unit Testing
Write comprehensive unit tests for your functions.
// Test framework setup (using Jest-like syntax)
class TestRunner {
constructor() {
this.tests = [];
this.passed = 0;
this.failed = 0;
}
test(name, fn) {
this.tests.push({ name, fn });
}
async run() {
for (const test of this.tests) {
try {
await test.fn();
console.log(`✓ ${test.name}`);
this.passed++;
} catch (error) {
console.log(`✗ ${test.name}: ${error.message}`);
this.failed++;
}
}
console.log(`\nTests: ${this.passed} passed, ${this.failed} failed`);
}
}
// Test utilities
function expect(actual) {
return {
toBe(expected) {
if (actual !== expected) {
throw new Error(`Expected ${expected}, but got ${actual}`);
}
},
toEqual(expected) {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(
`Expected ${JSON.stringify(expected)}, but got ${JSON.stringify(
actual
)}`
);
}
},
toThrow() {
try {
actual();
throw new Error("Expected function to throw an error");
} catch (error) {
// Expected behavior
}
},
};
}
// Example tests
const runner = new TestRunner();
runner.test("add function should return sum of two numbers", () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
runner.test("divide function should throw error for division by zero", () => {
expect(() => divide(5, 0)).toThrow();
});
runner.test("fetchUserData should return user object", async () => {
const user = await fetchUserData(1);
expect(user).toEqual({ id: 1, name: "John Doe" });
});
runner.run();
Mocking
Use mocking for testing functions with dependencies.
// Mock function
function createMock() {
const calls = [];
const mockFn = function (...args) {
calls.push(args);
return mockFn.returnValue;
};
mockFn.calls = calls;
mockFn.returnValue = undefined;
mockFn.mockReturnValue = (value) => {
mockFn.returnValue = value;
return mockFn;
};
return mockFn;
}
// Mock fetch
const mockFetch = createMock();
mockFetch.mockReturnValue({
ok: true,
json: () => Promise.resolve({ id: 1, name: "John Doe" }),
});
// Test with mock
runner.test("fetchUserData should use fetch", async () => {
const originalFetch = global.fetch;
global.fetch = mockFetch;
try {
await fetchUserData(1);
expect(mockFetch.calls.length).toBe(1);
expect(mockFetch.calls[0][0]).toBe("/api/users/1");
} finally {
global.fetch = originalFetch;
}
});