JavaScript Coding Guidelines
Build cleaner, more maintainable JavaScript by following practical standards used in real-world teams.
JavaScript coding guidelines are a defined set of rules, conventions, and best practices that developers follow to write clean, consistent, and maintainable code. A strong JavaScript style guide covers everything from naming conventions and variable declarations to error handling, code formatting, and project folder structure.
Why do JavaScript best practices matter in real projects?
In professional software development, codebases grow quickly and are maintained by multiple developers over months or years. Clear JavaScript coding guidelines make those codebases easier to review, test, extend, and debug. They do not replace good judgment, but they give a team a shared starting point.
- Reduces onboarding time for new team members
- Minimizes code review friction
- Prevents common JavaScript mistakes that lead to production issues
Who should follow these JavaScript coding standards?
Whether you are a beginner learning JavaScript, an intermediate developer preparing for coding interviews, or an experienced engineer leading a team, this guide is for you. Start with readable names and predictable control flow, then adapt the details to your project, tools, and team conventions.
This guide covers naming conventions, functions, variables, statements, error handling, scoping, formatting, comments, real project folder structure, common mistakes, and coding standards for JavaScript interviews. Each section includes practical examples you can apply immediately.
-
Class Names: Use UpperCamelCase
convention for naming them. UpperCamelCase means that
each word in the class name starts with a capital
letter, without any spaces or underscores between
them. For instance, if you have a class related to a
car, you might name it CarModel or CarController.
class TaskManager { constructor(projectName, ownerId) { this.projectName = projectName; this.ownerId = ownerId; this.tasks = []; } } -
Function Names & Variables: Use
lowerCamelCase. In lowerCamelCase, the first letter of
the word is in lowercase, while the first letter of
each subsequent word is capitalized. For example, if
you have a function that calculates the total price,
you might name it calculateTotalPrice or
getTotalAmount.
// Function name describes exactly what it does const formatUserGreeting = (firstName, isPremium) => isPremium ? `Welcome back, ${firstName}! Your premium perks are active.` : `Hello, ${firstName}! Ready to get started?`; // Variable names reflect what they store let pendingTaskCount = 4; let activeProjectName = 'Website Redesign'; -
Constants: Use
constwhen a binding should not be reassigned. ReserveUPPER_SNAKE_CASEfor shared, fixed values such as configuration constants or conversion factors. Local constants usually read better in lowerCamelCase. -
Variable Names
- Variable naming is crucial in JavaScript, and the key rule here is to make your variable names as descriptive as possible while reflecting their purpose or intention within your code. For instance, if you have a variable that stores a user's age, naming it something like userAge or ageOfUser would be descriptive and clear.
- Imagine you're explaining your code to someone who has no idea what it does. Your variable names should be so clear that this hypothetical person can easily understand what each variable is meant for just by reading its name.
-
Avoid generic names like temp, x, or data because
they don't convey much about the content or
purpose of the variable. Instead, opt for names
that provide context and meaning to anyone reading
your code.
// Clear names: anyone can read this without needing comments const cartItems = getCartItems(); const discountPercentage = 15; const subtotal = cartItems.reduce((sum, item) => sum + item.price, 0); const discountAmount = subtotal * (discountPercentage / 100); const finalTotal = subtotal - discountAmount; console.log(`Cart total after ${discountPercentage}% off: $${finalTotal.toFixed(2)}`); -
Short names are fine when their meaning is clear
from the local context. Common examples include
ifor a short loop,xandyfor coordinates, oridfor an identifier. Avoid short names when they hide important domain meaning.function filterUsers(users, role) { const matchingUsers = users; const requestedRole = role; return matchingUsers.filter(user => user.role === requestedRole); } // Short names are acceptable for well-known loop indices for (let i = 0; i < notifications.length; i++) { console.log(notifications[i].message); } -
Shortened names or abbreviations can obscure the
meaning and purpose of variables, leading to
confusion and making the code less readable. It
might save a few keystrokes initially, but it
sacrifices the clarity and understanding of the
code in the long run.
// Bad - abbreviated names obscure intent function filterUsers(u, r) { return u.filter(usr => usr.role === r); } // Good - descriptive names make the code self-documenting function filterUsersByRole(users, role) { return users.filter(user => user.role === role); }
const MAX_RETRY_ATTEMPTS = 3;
const FREE_SHIPPING_THRESHOLD = 50; // in USD
function getCheckoutTotal(subtotal, taxRate) {
const tax = subtotal * taxRate; // local const: lowerCamelCase
return subtotal + tax;
}
-
Use arrow functions when they make a small callback
or stateless helper easier to read. Use a regular
function when you need a dynamic
this, a constructor, a named function declaration, or clearer step-by-step control flow.// Traditional function - good when you need 'this' or multiple statements function applyDiscountWithFunction(price, discountRate) { return price - price * discountRate; } // Arrow function - cleaner for simple, stateless operations const applyDiscount = (price, discountRate) => price - price * discountRate; console.log(applyDiscount(120, 0.20)); // Output: 96 (20% off $120) -
Documenting functions with inputs, outputs, and
potential errors is a fantastic practice for improving
code clarity and assisting anyone who uses or
maintains your code.
/** * Applies a percentage discount to a price. * @param {number} originalPrice - The original product price (must be >= 0). * @param {number} discountRate - Discount as a decimal: 0.2 = 20% off. * @returns {number} The price after discount, rounded to 2 decimal places. * @throws {Error} If either argument is not a valid non-negative number. */ function applyDiscount(originalPrice, discountRate) { if (typeof originalPrice !== 'number' || originalPrice < 0) { throw new Error('originalPrice must be a non-negative number.'); } if (typeof discountRate !== 'number' || discountRate < 0 || discountRate > 1) { throw new Error('discountRate must be a decimal between 0 and 1.'); } return parseFloat((originalPrice * (1 - discountRate)).toFixed(2)); } try { console.log(applyDiscount(199.99, 0.25)); // Output: 149.99 console.log(applyDiscount(-10, 0.1)); // Throws } catch (error) { console.error(error.message); } - Keep documentation up-to-date with code changes. Keeping documentation up-to-date is as important as writing it in the first place. As your code evolves, it's crucial to update the associated documentation to ensure it remains accurate and reflects the current behavior of your functions.
-
While documentation for getters and setters in
JavaScript isn't mandatory, it's still beneficial to
provide some form of documentation, especially for
complex or crucial properties.
class NotificationService { constructor(channel) { this._channel = channel; // 'email', 'sms', or 'push' } get channel() { return this._channel; } set channel(newChannel) { const valid = ['email', 'sms', 'push']; if (!valid.includes(newChannel)) { throw new Error(`Invalid channel. Choose: ${valid.join(', ')}`); } this._channel = newChannel; } } const notifier = new NotificationService('email'); console.log(notifier.channel); // 'email' notifier.channel = 'push'; console.log(notifier.channel); // 'push' notifier.channel = 'fax'; // Throws: Invalid channel -
Documenting classes with descriptions of their
purposes is a great practice that enhances code
readability and helps developers understand the role
of each class within a codebase.
/** * Manages notification delivery across different channels. * Validates input and provides controlled access to the active channel. * @class */ class NotificationService { /** * @param {string} channel - Delivery channel ('email' | 'sms' | 'push'). */ constructor(channel) { this._channel = channel; } /** * Returns the current delivery channel. * @returns {string} */ get channel() { return this._channel; } /** * Updates the delivery channel. * @param {string} newChannel - Must be 'email', 'sms', or 'push'. * @throws {Error} If the channel is not one of the allowed values. */ set channel(newChannel) { const valid = ['email', 'sms', 'push']; if (!valid.includes(newChannel)) { throw new Error(`Invalid channel. Choose: ${valid.join(', ')}`); } this._channel = newChannel; } } -
Avoid functions with many positional arguments; use
an options object when named values make a call
easier to read. There is no universal maximum, but
long argument lists can make calls harder to review
and easier to get wrong.
// Bad - too many individual arguments; easy to pass them in wrong order function createUserAccount(firstName, lastName, email, password, role, department, phone) { // What if the caller swaps 'role' and 'department'? No error, silent bug. } createUserAccount('Jane', 'Doe', 'jane@example.com', 'pass', 'Marketing', 'editor', '+1-555-0100'); // Good - a single options object is self-documenting and order-independent function createUserAccount(userDetails) { const { firstName, lastName, email, password, role, department, phone } = userDetails; // ... } createUserAccount({ firstName: 'Jane', lastName: 'Doe', email: 'jane@example.com', password: 'SecurePass1!', role: 'editor', department: 'Marketing', phone: '+1-555-0100', }); -
Avoid overly long methods; keep methods ideally
fitting within a single screen view. Refactor as
needed. By refactoring long methods into smaller, more
focused ones, you enhance code readability,
maintainability, and make it easier for others (and
your future self!) to understand and modify the
codebase.
// Bad - one function trying to do everything at once async function processNewOrder(order) { // validate order items // calculate totals and taxes // apply discount codes // charge the customer // send confirmation email // update inventory // ... 80+ lines that are impossible to test in isolation }// Good - each function has one clear job and can be tested independently function validateOrderItems(items) { /* check stock, required fields */ } function calculateOrderTotal(items, tax) { /* returns { subtotal, tax, total } */ } function chargeCustomer(payment, amount) { /* call payment API */ } function sendConfirmationEmail(order) { /* send receipt */ } function updateInventory(items) { /* decrement stock */ } async function processNewOrder(order) { validateOrderItems(order.items); const totals = calculateOrderTotal(order.items, order.taxRate); await chargeCustomer(order.payment, totals.total); await sendConfirmationEmail({ ...order, totals }); updateInventory(order.items); } -
Consider redesigning classes with too many fields.
When a class contains an excessive number of fields,
it might be a sign that the class is taking on too
many responsibilities and violating the Single
Responsibility Principle (SRP). Redesigning such
classes can improve code readability and
maintainability.
// Bad - one class trying to handle everything about a product listing class ProductListing { constructor(title, price, currency, category, brandName, sku, weight, dimensions, color, stockQuantity) { // 10+ fields is a red flag - this class has too many responsibilities } }// Good - split by responsibility, each class stays focused class ProductMeta { constructor(title, category, brandName, sku) { /* ... */ } } class ProductPhysical { constructor(weight, dimensions, color) { /* ... */ } } class ProductInventory { constructor(price, currency, stockQuantity) { /* ... */ } } class ProductListing { constructor(meta, physical, inventory) { this.meta = meta; // ProductMeta instance this.physical = physical; // ProductPhysical instance this.inventory = inventory; // ProductInventory instance } } -
Each function should have a single responsibility.
adhering to the Single Responsibility Principle (SRP)
is crucial for writing clean and maintainable code.
This principle suggests that each function should have
a single, well-defined responsibility.
// Bad - one function doing two unrelated jobs async function saveCommentAndNotify(comment, userId) { await db.comments.insert(comment); // job 1: persistence await emailService.send(userId, comment); // job 2: notification // If the email fails, do we roll back the comment? Hard to reason. }// Good - each function owns exactly one responsibility async function saveComment(comment) { return await db.comments.insert(comment); } async function sendCommentNotification(userId, commentId) { return await emailService.send(userId, { commentId }); } // Caller decides when and whether to notify async function saveAndNotify(comment, userId) { const saved = await saveComment(comment); if (saved) await sendCommentNotification(userId, saved.id); } -
Use plural naming convention for methods returning
arrays. Adopting a plural naming convention for
methods returning arrays is a helpful practice that
enhances code readability and communicates the return
type effectively.
class ShoppingCart { constructor() { this.items = []; } // Method returning an array of items getItems() { return this.items; } // Other methods for adding, removing, or manipulating items in the cart addItem(item) { this.items.push(item); } // ... } // Usage const cart = new ShoppingCart(); cart.addItem('Apple'); cart.addItem('Banana'); const items = cart.getItems(); console.log(items); // Output: ['Apple', 'Banana'] - Place new functions where their dependencies and responsibilities are easy to follow. Group related helpers together, keep public entry points easy to find, and let your formatter and module structure guide the final order. There is no universal rule that functions must be added only at the top or bottom of a file.
-
Use
constandletinstead ofvar. This shift away from var brings better scoping and reduces the risk of unintended reassignments or scope issues within your code. It's a cleaner and more predictable way to manage variables in JavaScript!const MAX_LOGIN_ATTEMPTS = 5; // const: value never changes let loginAttempts = 0; // let: this will be incremented loginAttempts++; // fine loginAttempts++; // fine // MAX_LOGIN_ATTEMPTS = 10; // TypeError: Assignment to constant variable -
Avoid global variables. By minimizing the use of
global variables and instead utilizing local scopes or
appropriate data passing techniques, you create more
robust and maintainable code, reducing the chances of
unintended side effects and enhancing code
readability.
// Bad - global variable that any function can accidentally change let globalCartTotal = 0; function applyDiscount() { globalCartTotal = globalCartTotal * 0.9; // silently modifies the global } // Good - pass values as arguments, return the result function applyDiscount(total, rate) { return total * (1 - rate); } let cartTotal = 150; cartTotal = applyDiscount(cartTotal, 0.1); // 135 -
Declare variables near the code that first uses them,
and choose the narrowest scope that fits. This keeps
related logic together and avoids unnecessary
hoisting-related confusion.
function buildOrderSummary(items, taxRate) { const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0); const taxAmount = subtotal * taxRate; const grandTotal = subtotal + taxAmount; return { subtotal, taxAmount, grandTotal }; } -
Initialize local variables with a meaningful default
when the logic needs one. This makes the intended
empty or starting state clear without hiding missing
or invalid input behind arbitrary fallback values.
function getPageMetrics(entries) { // Default values prevent NaN or 'undefined' from leaking into calculations let totalViews = 0; let uniqueVisitors = 0; let avgBounceRate = 0; if (!entries || entries.length === 0) { return { totalViews, uniqueVisitors, avgBounceRate }; } entries.forEach(entry => { totalViews += entry.views; uniqueVisitors += entry.uniqueVisitors; avgBounceRate += entry.bounceRate; }); avgBounceRate = avgBounceRate / entries.length; return { totalViews, uniqueVisitors, avgBounceRate }; } -
Remove unused parameters and variables. By removing
unused parameters and variables, you streamline your
code, making it more manageable, easier to understand,
and potentially improving its performance. This
practice also encourages a clean and focused codebase.
// Bad - 'status' is declared but never used function getActiveUsers(users, status) { return users.filter(u => u.isActive); // 'status' is ignored - dead code } // Good - only accept what you actually use function getActiveUsers(users) { return users.filter(u => u.isActive); } -
Use strict equality (
===) over loose equality (==). By using strict equality (===), you promote code reliability and avoid potential bugs caused by implicit type conversions, leading to more predictable and safer code.const userInput = '0'; const adminCode = 0; // Loose equality - dangerous type coercion console.log(userInput == false); // true! ('0' coerces to 0, then to false) console.log(adminCode == false); // true! (0 coerces to false) // Strict equality - no surprises console.log(userInput === false); // false (string !== boolean) console.log(adminCode === false); // false (number !== boolean) console.log(adminCode === 0); // true (correct check)
-
Limit one statement per line. While JavaScript allows
multiple statements on a single line separated by
semicolons, separating each statement onto its own
line improves code clarity and makes it easier to
understand and work with, especially in complex or
large codebases.
const itemPrice = 49.99; const taxRate = 0.08; const totalAmount = itemPrice * (1 + taxRate); -
Choose a semicolon style and enforce it with your
formatter and linter. This page uses semicolons, which
makes statement boundaries explicit, but a consistent
semicolon-free style is also valid when the project
tools handle automatic semicolon insertion safely.
// Bad - missing semicolons, relying on ASI const usernameWithoutSemicolons = 'alice' const cartCountWithoutSemicolons = 3 console.log(usernameWithoutSemicolons) // Good - semicolons remove any ambiguity const username = 'alice'; const cartCount = 3; console.log(username); -
Remove duplicate statements. By actively searching for
and removing duplicate statements, you ensure that
your code is cleaner, more efficient, and easier to
comprehend, contributing to better maintainability and
readability.
const originalPrice = 100; let discountedPrice = originalPrice * 0.9; discountedPrice = originalPrice * 0.9; // Duplicate - same value computed twice console.log(discountedPrice); // 90 -
Use ternary operators for simple two-branch
expressions. Prefer
if/elsewhen conditions are nested or the branches contain several statements.const itemCount = 5; // With if/else let message; if (itemCount > 0) { message = `You have ${itemCount} item(s) in your cart.`; } else { message = 'Your cart is empty.'; } // With ternary - same result, more concise const cartMessage = itemCount > 0 ? `You have ${itemCount} item(s) in your cart.` : 'Your cart is empty.'; -
Include braces for one-line
if,else, and loop bodies. Braces make future edits safer and keep control flow easy to scan.// Bad - easy to make a mistake when adding a second line later if (isLoggedIn) redirectToDashboard(); // Good - braces make intent clear and guard against future edits if (isLoggedIn) { redirectToDashboard(); }
-
Handle errors at the level that has enough context to
recover, report, transform, or respond appropriately.
Use
try...catcharound operations that can throw when the current layer can do something useful; otherwise, let the error propagate to a boundary that can handle it. Avoid catching an error only to log it and rethrow it again at every layer.// Using try-catch try { const user = JSON.parse(rawUserData); renderUserProfile(user); } catch (error) { console.error('Could not parse user data:', error.message); } // Using a centralized error-handling function function handleError(error) { console.error('[App Error]', error.message); showToast('Something went wrong. Please try again.'); } async function loadSettingsSafely(userId) { try { await loadUserSettings(userId); } catch (error) { handleError(error); } } -
Avoid empty catch blocks; log, comment, or perform
meaningful logic. By avoiding empty catch blocks and
incorporating meaningful error handling, you improve
code maintainability, aid in debugging, and ensure a
more robust error management strategy in your
JavaScript applications.
// Bad - empty catch swallows the error silently try { const config = JSON.parse(configString); } catch (error) { // nothing here - you'll never know why the app broke } // Good - log it and provide a fallback let config; try { config = JSON.parse(configString); } catch (error) { console.error('Failed to parse config, using defaults:', error.message); config = getDefaultConfig(); } // Good - type-specific handling async function connectSafely() { try { await connectToDatabase(); } catch (error) { if (error.code === 'ECONNREFUSED') { console.error('Database is offline:', error.message); notifyOpsTeam(error); } else { console.error('Unexpected DB error:', error); } } } -
Place cleanup logic in the
finallyblock if needed. By placing cleanup logic in the finally block, you ensure that essential cleanup operations occur, promoting more reliable and resilient code in scenarios where resource management or final operations are crucial.async function exportReport(reportId) { let dbConnection; try { dbConnection = await openDatabaseConnection(); const data = await fetchReportData(reportId); return formatReportAsPDF(data); } catch (error) { console.error('Export failed:', error.message); throw error; // re-throw so the caller knows it failed } finally { // Runs whether export succeeded or failed - always close the connection if (dbConnection) await dbConnection.close(); } } -
Keep the code that creates an error separate from the
code that can meaningfully handle it when practical.
Catching in the same layer is appropriate when that
layer can add context, recover, or translate the
error. Do not catch and rethrow unchanged errors just
to add another log entry.
try { // Code that might throw an error if (condition) { throw new Error('Some error occurred'); } // Catching the error immediately } catch (error) { console.error('Caught the error:', error.message); // Handling the error here } //Example with Propagation: function someFunction() { if (condition) { throw new Error('Some error occurred'); } } try { someFunction(); } catch (error) { console.error('Error occurred:', error.message); // Handle or log the error at an appropriate level } -
Let errors bubble where appropriate and handle them
logically. By letting errors bubble up appropriately
in your code and handling them logically at higher
levels, you maintain a clear and organized
error-handling structure, simplifying debugging and
ensuring consistent error management throughout your
application.
// Error originates deep in a utility function function parseUserData(rawJson) { if (!rawJson || typeof rawJson !== 'string') { throw new TypeError('rawJson must be a non-empty string'); } return JSON.parse(rawJson); // may also throw if invalid JSON } // Caller handles it at the right level - has context to recover gracefully try { const user = parseUserData(apiResponse); renderUserCard(user); } catch (error) { console.error('Could not render user card:', error.message); renderFallbackCard(); } -
For API calls, check the response status and handle
the failure where the application can choose an
appropriate user response or retry policy. Preserve
useful status and response details, and log once at
the boundary responsible for observability.
fetch('https://api.example.com/data') .then(response => { if (!response.ok) { throw new Error(`Request failed with status: ${response.status}`); } return response.json(); }) .catch(error => { console.error('API Error:', error.message); // Log the status code for further investigation if (error.message.includes('status')) { const statusCode = error.message.split(': ')[1]; console.error('Status Code:', statusCode); } // Handle or display the error appropriately });
- Clean up all warnings before check-in (if possible, configure in save actions). By adopting these practices and tools, you maintain a cleaner and more consistent codebase, ensuring better code quality and reducing the likelihood of potential issues or bugs.
- Format the code before check-in (if possible, configure in save actions). By configuring save actions to format code automatically before check-in, you maintain a consistent code style, improve readability, and streamline collaboration among developers working on the same codebase.
- To configure save actions for formatting in Visual Studio Code (VS Code), you can install extensions like "Prettier" and set the "editor.formatOnSave" option to true in your settings.
- Use template literals for dynamic strings. Using template literals for dynamic strings enhances readability, simplifies string interpolation, and facilitates the creation of complex strings by embedding variables or expressions directly within the string template.
// VS Code settings.json
{
"editor.formatOnSave": true
}
// Without template literals
let name = 'JSChamp';
let greeting = 'Hello, ' + name + '! How are you today?';
// With template literals
let greetingTemplate = `Hello, ${name}! How are you today?`;
// Multiline string using template literals
let multiLineString = `This is a
multiline
string
created with
template literals.`;
// Expressions within template literals
let num1 = 5;
let num2 = 10;
let result = `The sum of ${num1} and ${num2} is ${num1 + num2}.`;
- Provide the most restricted scope for variables and functions (if used in class). By limiting the scope of variables and functions within classes, you enforce encapsulation, providing a clearer and more organized structure to your code, thus improving its maintainability and preventing unintended interference.
- Global Functions are public only if part of a Global scope. By understanding the concept of global scope and encapsulation within modules or closures, you can control the accessibility of functions, ensuring a more structured and organized codebase.
- Getters/setters in a class may remain public if not consumed externally. By considering the current and future requirements of the class and its properties, you can decide whether to keep getters/setters public or encapsulate them for more controlled access within the class.
- Member and static variables (excluding constants) should be private. By making member (instance) and static variables private and providing controlled access through methods or interfaces, you ensure better encapsulation, stronger data integrity, and a more maintainable codebase.
class UserSession {
#token; // private - only accessible inside this class
constructor(token) {
this.#token = token;
}
#isExpired() { // private helper - no need to expose this
return Date.now() > this.#token.expiresAt;
}
getUser() { // public - this is what callers use
if (this.#isExpired()) throw new Error('Session has expired.');
return this.#token.user;
}
}
// Globally accessible - intentionally public
function formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}
formatCurrency(49.99); // '$49.99' - usable from anywhere
// Module pattern - internal logic stays private, only the API is exposed
const CartModule = (function () {
let items = []; // private to this closure
function recalculateTotal() {
return items.reduce((sum, item) => sum + item.price, 0);
}
return {
addItem(item) { items.push(item); },
getTotal() { return recalculateTotal(); },
};
})();
CartModule.addItem({ price: 29.99 });
console.log(CartModule.getTotal()); // 29.99
class Notification {
constructor(message) {
this._message = message;
}
get message() {
return this._message;
}
set message(newMessage) {
this._message = newMessage.trim();
}
}
const alert = new Notification(' Your order has shipped! ');
console.log(alert.message); // 'Your order has shipped!'
alert.message = ' Package delivered! ';
console.log(alert.message); // 'Package delivered!'
// With private fields (ES2022+)
class PrivateNotification {
#message;
constructor(message) { this.#message = message.trim(); }
getMessage() { return this.#message; }
setMessage(msg) { this.#message = msg.trim(); }
}
class RateLimiter {
#requestCount = 0;
static #maxRequests = 100;
get requestCount() {
return this.#requestCount;
}
set requestCount(value) {
if (value < 0) throw new Error('Count cannot be negative.');
this.#requestCount = value;
}
static get maxRequests() {
return RateLimiter.#maxRequests;
}
static set maxRequests(value) {
if (value < 1) throw new Error('Max must be at least 1.');
RateLimiter.#maxRequests = value;
}
}
- Use comments to explain why the code is complex, surprising, or constrained. Prefer clear names and structure for explaining what straightforward code does, and remove comments that become inaccurate.
- Remove commented code before check-in. By removing commented-out code before check-in, you ensure that the codebase stays organized, making it easier for collaborators to understand the current state of the code and maintain its cleanliness for future development.
- Keep comments synchronized with the implementation as the code changes. Use JSDoc for public APIs, reusable libraries, or genuinely complex contracts when generated documentation or editor support is useful.
// A debounce utility - complex enough to benefit from a comment
// Delays execution until the user stops triggering the event for `delayMs`.
// Prevents expensive work (API calls, DOM updates) on every keystroke.
function debounce(fn, delayMs) {
let timeoutId;
return function (...args) {
// Cancel any pending invocation before scheduling a new one
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delayMs);
};
}
// Usage: search fires only after the user stops typing for 300ms
searchInput.addEventListener('input', debounce(() => {
fetchSearchResults(searchInput.value);
}, 300));
// Bad - commented-out code clutters the codebase; others don't know if it's important
// function getLegacyUser(id) {
// return db.query(`SELECT * FROM users_v1 WHERE id = ${id}`);
// }
// Good - delete it. Git history keeps the record if you ever need it back.
// Price is passed in cents to avoid common floating-point rounding errors.
// This business rule rounds tax before adding it; other domains may use a
// different documented rounding policy.
function computeFinalPrice(priceInCents, taxRate) {
const taxAmount = Math.round(priceInCents * taxRate);
const totalInCents = priceInCents + taxAmount;
// Convert to dollars only for display - never store prices as floats
return totalInCents / 100;
}
- Keep classes focused and reasonably manageable. A very large class can signal too many responsibilities, but line count alone should not decide when to refactor. Teams may use a practical limit such as 400 lines as a review prompt, then consider cohesion, complexity, and responsibility boundaries.
- By adhering to a reasonable limit on class size, you promote maintainability and readability, making it easier for developers to understand and work with the codebase over time.
- Keep functions focused and reasonably small. A long function can signal that responsibilities should be split, but line count alone should not determine when to refactor. Consider complexity, branching, side effects, and readability rather than treating 75 lines as a language rule.
- By adhering to reasonable limits on function size, you improve code readability, maintainability, and understanding, allowing for more efficient development and easier collaboration among team members.
// When one class starts collecting unrelated responsibilities, it's a sign to split it
class OrderManager {
constructor() { /* ... */ }
// Order creation & validation
createOrder(items, customer) { /* ... */ }
validateOrderItems(items) { /* ... */ }
// Payment - arguably its own PaymentService
chargeCard(card, amount) { /* ... */ }
sendPaymentReceipt(email) { /* ... */ }
// Shipping - arguably its own ShippingService
scheduleDelivery(order) { /* ... */ }
trackShipment(orderId) { /* ... */ }
// Grew past ~400 lines? Time to split into focused services.
}
- Eliminate unnecessary checks in the code. By removing unnecessary checks, you not only improve the performance of your code but also enhance its readability and maintainability by eliminating redundant logic.
// Before - checking for both undefined and null separately
function processValueBefore(value) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return 'default';
}
// After - use nullish coalescing (??) for null/undefined checks.
// Don't use !value because it also blocks 0, "", and false
function processValue(value) {
return String(value ?? 'default').toUpperCase();
}
// Another common pattern - early return with null check
function getDiscount(price, discount) {
if (price == null || discount == null) return 0;
return price * discount;
}
- Choose the programming style that fits the problem. JavaScript supports object-oriented, functional, procedural, and mixed approaches. Encapsulation and polymorphism can help in an object-oriented design, but they are not requirements for every application. Prefer composition over inheritance when assembling smaller behaviors produces a simpler design.
- Use language APIs that are appropriate for your supported runtimes. Current features can improve clarity, but check browser or Node.js support, transpilation targets, and team conventions before adopting them.
- Extract reusable utilities when repetition is real and the abstraction has a clear purpose. A small function or module is often simpler than a utility class; choose the form that fits the behavior and avoids a catch-all helpers module.
- Use inheritance when the subtype relationship is stable and the shared contract is clear. Otherwise, composition, delegation, or small modules may be easier to change and test. Inheritance is an option, not a requirement for reuse.
- Use modular code and break it into smaller, reusable modules. By adopting modular code practices and breaking down functionalities into smaller, reusable modules, you enhance code maintainability, reusability, and scalability, making your codebase more manageable and adaptable to changes.
-
Prefer Promises and async/await when they make a
sequential asynchronous workflow easier to read.
Callbacks remain appropriate for event listeners,
callback-based APIs, and code where they make control
flow clear. Independent operations can often start
together and be awaited with
Promise.all()rather than being awaited sequentially. -
Avoid using
eval(). By avoiding the use of eval() and opting for safer alternatives or approaches, you mitigate security risks, improve performance, and maintain the readability and maintainability of your codebase. - Avoid magic numbers; use constants or variables. By replacing magic numbers with meaningful constants or variables, you enhance code readability and maintainability, reduce the risk of errors, and ensure consistency throughout the codebase.
- Avoid deeply nested callbacks when they obscure the control flow. Promises or async/await often make sequential workflows easier to follow, but a callback is not inherently incorrect. Use the style that fits the API and keeps ownership, cleanup, and errors clear.
- Use destructuring for object and array manipulation. By utilizing destructuring for array and object manipulation, you can streamline code, make it more expressive, and facilitate the extraction of values from complex data structures with ease.
-
Avoid relying on the global object (e.g.,
windoworglobal). By minimizing reliance on the global object and embracing module-based development practices, you can write more modular, maintainable, and scalable code, leading to better code organization and easier debugging. - Utilize ES6 features. By utilizing ES6 features, you can write more modern, expressive, and efficient JavaScript code, enabling better development practices and enhancing the overall quality of your applications.
- Use linters (e.g., ESLint) for code standards enforcement. By integrating ESLint or similar linters into your workflow, you ensure code consistency, catch potential issues early, and foster a healthier and more standardized codebase across your projects.
-
Choose the iteration method that matches the task.
Use
forEach()for a simple side effect,map()when transforming values, and afor...ofloop when you needbreak,continue, orawaitinside the loop. There is no need to replace everyforloop withforEach(). -
Consider
Object.freeze()for small lookup objects or public configuration that should not be mutated. It is shallow, does not validate input, and may be unnecessary when modules already keep a value private. Freeze nested objects separately or use a different strategy when deep immutability is needed. -
Use JSON.stringify()to serialize objects. By utilizing JSON.stringify() effectively, you can convert JavaScript objects into a standardized JSON string format, enabling seamless data interchange, storage, and communication across different platforms and systems.const cartSnapshot = { userId: 'usr_abc123', items: [ { productId: 'p1', name: 'Wireless Mouse', qty: 1, price: 29.99 }, { productId: 'p2', name: 'USB Hub', qty: 2, price: 19.99 }, ], couponCode: 'SAVE10', }; // Serialize for an API request body or localStorage const serialized = JSON.stringify(cartSnapshot); console.log(serialized); // {"userId":"usr_abc123","items":[...],"couponCode":"SAVE10"} // Pretty-print for debugging console.log(JSON.stringify(cartSnapshot, null, 2)); -
Use console.log()for debugging; remove logs in production code. By using console.log() for debugging during development and subsequently removing or disabling these logs from production code, you maintain a cleaner and more secure codebase while still benefiting from effective debugging aids during development.
// Polymorphism - same method name, different behaviour per subclass
class Notification {
send() { /* base */ }
}
class EmailNotification extends Notification {
send(recipient, message) {
console.log(`Emailing ${recipient}: ${message}`);
}
}
class PushNotification extends Notification {
send(deviceToken, message) {
console.log(`Push to ${deviceToken}: ${message}`);
}
}
// Inheritance - extend a base class to share common logic
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() { return `Hello, ${this.name}!`; }
}
class AdminUser extends User {
constructor(name, email, accessLevel) {
super(name, email);
this.accessLevel = accessLevel;
}
greet() { return `${super.greet()} [Admin L${this.accessLevel}]`; }
}
// Encapsulation - hide internal state, expose a safe API
class BankAccount {
#balance = 0;
deposit(amount) {
if (amount <= 0) throw new Error('Amount must be positive.');
this.#balance += amount;
}
getBalance() { return this.#balance; }
}
// Utility classes group pure, stateless helpers under one namespace
class StringUtils {
static capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
static slugify(str) {
return str.toLowerCase().trim()
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '');
}
static truncate(str, maxLength) {
return str.length > maxLength ? `${str.slice(0, maxLength)}…` : str;
}
}
StringUtils.slugify('Hello World!'); // 'hello-world'
StringUtils.capitalize('jAVASCRIPT'); // 'Javascript'
StringUtils.truncate('Long article title here', 20); // 'Long article title h…'
// BaseWidget handles common UI concerns so subclasses focus on their unique logic
class BaseWidget {
constructor(id) {
this.id = id;
this.isVisible = true;
}
show() { this.isVisible = true; }
hide() { this.isVisible = false; }
}
class DropdownWidget extends BaseWidget {
constructor(id, options) {
super(id);
this.options = options;
this.selectedIndex = 0;
}
select(index) {
this.selectedIndex = index;
console.log(`Selected: ${this.options[index]}`);
}
}
const dropdown = new DropdownWidget('size-picker', ['S', 'M', 'L', 'XL']);
dropdown.select(2); // Selected: L
dropdown.hide(); // inherited from BaseWidget
// Bad - what do 86400 and 0.15 mean here?
function processExpiredSessions(sessions) {
return sessions.filter(s => s.age > 86400 && s.riskScore < 0.15);
}
// Good - constants make the intent self-documenting
const ONE_DAY_IN_SECONDS = 86_400;
const SUSPICIOUS_SCORE_LIMIT = 0.15;
function processExpiredSessions(sessions) {
return sessions.filter(s =>
s.age > ONE_DAY_IN_SECONDS && s.riskScore < SUSPICIOUS_SCORE_LIMIT
);
}
// Bad - callback hell (each step nests deeper)
getUserProfile(userId, (profile) => {
getFollowers(profile.id, (followers) => {
getLatestPosts(profile.id, (posts) => {
renderDashboard(profile, followers, posts); // buried 3 levels deep
});
});
});
// Good - flat and readable with async/await
async function loadDashboard(userId) {
const profile = await getUserProfile(userId);
const followers = await getFollowers(profile.id);
const posts = await getLatestPosts(profile.id);
renderDashboard(profile, followers, posts);
}
const notifications = [
{ id: 1, message: 'New follower', read: false },
{ id: 2, message: 'Comment on your post', read: false },
{ id: 3, message: 'Your order has shipped', read: true },
];
// Traditional for loop - extra index variable to manage
for (let i = 0; i < notifications.length; i++) {
console.log(notifications[i].message);
}
// forEach - no index needed, reads like plain English
notifications.forEach(notification => {
console.log(notification.message);
});
// Bonus: chain with filter to only show unread ones
notifications
.filter(n => !n.read)
.forEach(n => console.log(`Unread: ${n.message}`));
// Freeze constant lookup tables so values can never be accidentally changed
const HTTP_STATUS = Object.freeze({
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
NOT_FOUND: 404,
SERVER_ERROR: 500,
});
// Attempts to add or change are silently ignored (or throw in strict mode)
HTTP_STATUS.OK = 999; // ignored
HTTP_STATUS.TIMEOUT = 408; // ignored
console.log(HTTP_STATUS.OK); // 200
console.log(HTTP_STATUS.NOT_FOUND); // 404
A thoughtful project structure can improve readability, navigation, and collaboration, but there is no single universal JavaScript folder layout. The best choice depends on the application type, framework, project size, team conventions, feature boundaries, and build tooling. The structure below is one practical example for a small or medium application.
my-project/
+-- src/
| +-- components/ # Reusable UI components
| | +-- Button.js
| | +-- Modal.js
| | +-- Navbar.js
| +-- services/ # API calls and business logic
| | +-- authService.js
| | +-- userService.js
| +-- utils/ # Helper/utility functions
| | +-- formatDate.js
| | +-- validators.js
| +-- constants/ # App-wide constants
| | +-- config.js
| +-- styles/ # CSS or SCSS files
| | +-- main.css
| | +-- variables.css
| +-- tests/ # Unit and integration tests
| | +-- Button.test.js
| | +-- authService.test.js
| +-- app.js # Main application entry point
| +-- index.js # App bootstrapping
+-- public/ # Static assets (images, fonts)
+-- .eslintrc.js # ESLint configuration
+-- .prettierrc # Prettier configuration
+-- package.json # Dependencies and scripts
+-- README.md # Project documentation
Key JavaScript best practices for folder structure:
- Group files by feature or responsibility when that makes related code easier to find. Some projects intentionally group by file type, especially when they are small or follow a framework convention.
-
Keep shared utility functions in a clear location,
such as a
utils/folder, when that improves discoverability. Avoid a catch-all folder that becomes difficult to navigate. - Separate API service calls from UI components to follow the single responsibility principle.
- Store configuration and constants in a centralized location to avoid hardcoding values.
-
Keep tests close to their source or in a dedicated
tests/folder, depending on the test runner and team convention. -
Use a linter such as ESLint and a formatter such as
Prettier when they solve a project need. Configuration
may live in flat config files, package.json, or tool-
specific files, so do not assume that every project
should contain
.eslintrc.jsand.prettierrc.
Even experienced developers make common JavaScript coding mistakes. Understanding these pitfalls is an essential part of following JavaScript coding guidelines. Avoiding these mistakes will help you write more reliable, secure, and maintainable code.
-
Using
==instead of===: The loose equality operator performs type coercion, which can surprise readers. Prefer strict equality for most application code, but understand the coercion rules and document deliberate exceptions.// ? Bad - loose equality if (value == '5') { /* runs even if value is number 5 */ } // ? Good - strict equality if (value === '5') { /* only runs if value is string '5' */ } -
Not handling asynchronous errors:
Unhandled Promise rejections are a real reliability
problem. Handle a rejection where the application has
enough context to recover, report, or present an
error; that does not require wrapping every
awaitin its owntry...catch.// ? Bad - no error handling async function fetchData() { const response = await fetch('/api/data'); return response.json(); } // ? Good - proper error handling async function fetchData() { try { const response = await fetch('/api/data'); return response.json(); } catch (error) { console.error('Failed to fetch data:', error); throw error; } } -
Modifying objects or arrays passed as function arguments:
Mutating input parameters causes unexpected side effects. Follow JavaScript best practices by creating copies instead.
// ? Bad - mutating the original array function addItem(list, item) { list.push(item); return list; } // ? Good - returning a new array function addItem(list, item) { return [...list, item]; } - Declaring variables in the global scope: Unintended global variables cause naming collisions and are hard to debug. Prefer module, function, or block scope, while intentionally public globals should be documented and kept to a minimum.
-
Ignoring
nullandundefinedchecks: Accessing properties onnullorundefinedis one of the most common runtime errors. Use optional chaining (?.) and nullish coalescing (??) to write safer code.// ? Bad - may throw TypeError const unsafeCity = user.address.city; // ? Good - safe property access const city = user?.address?.city ?? 'Unknown'; -
Not using
constby default: Preferconstwhen a binding is not reassigned and useletwhen it is. In modern code, avoidvarunless you specifically need its function-scoped behavior or are maintaining older code.
Writing clean JavaScript is not just about following rules - it is about building code that your team can read, debug, and extend without frustration. Below are real-world before-and-after examples that show the practical difference between bad and good JavaScript code in production applications.
1. Fetching and displaying user data
This is one of the most common tasks in frontend and backend development. Bad code often mixes concerns, skips error handling, and uses unclear variable names.
// ? Bad: No error handling, unclear naming, mixed concerns
var d = fetch('/api/users');
d.then(function(r) {
var x = r.json();
x.then(function(data) {
var el = document.getElementById('output');
var h = '';
for (var i = 0; i < data.length; i++) {
h += '<p>' + data[i].name + '</p>';
}
el.innerHTML = h;
});
});
// ? Good: Clear naming, error handling, separated concerns, modern syntax
const fetchUsers = async () => {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
console.error('Failed to fetch users:', error);
return [];
}
};
const renderUserList = (users) => {
const outputElement = document.getElementById('output');
if (!users.length) {
outputElement.textContent = 'No users found.';
return;
}
const userElements = users.map((user) => {
const paragraph = document.createElement('p');
paragraph.textContent = user.name;
return paragraph;
});
outputElement.replaceChildren(...userElements);
};
// Usage
async function renderPage() {
const users = await fetchUsers();
renderUserList(users);
}
renderPage();
Why the good version is better:
- Uses
async/awaitinstead of nested.then()chains for readability. - Handles HTTP errors and network failures with
try...catch. - Separates data fetching from DOM rendering, following the separation of concerns principle.
- Uses
constinstead ofvar, and descriptive names likefetchUsersandrenderUserList. - Handles the empty state gracefully instead of rendering nothing.
2. Validating form input
Form validation is where messy code often hides in real projects. A well-structured approach prevents bugs and makes the logic easy to extend.
// ? Bad: Repetitive, hard to extend, unclear error messages
function validate(f) {
if (f.name == '' || f.name == null) {
alert('error');
return false;
}
if (f.email == '' || f.email == null) {
alert('error');
return false;
}
if (f.password.length < 8) {
alert('error');
return false;
}
return true;
}
// ? Good: Reusable validators, clear messages, easy to extend
const validationRules = {
name: (value) => value?.trim() ? '' : 'Name is required.',
email: (value) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? '' : 'Enter a valid email address.',
password: (value) =>
value?.length >= 8 ? '' : 'Password must be at least 8 characters.',
};
const validateForm = (formData) => {
const errors = {};
for (const [field, rule] of Object.entries(validationRules)) {
const errorMessage = rule(formData[field]);
if (errorMessage) {
errors[field] = errorMessage;
}
}
return {
isValid: Object.keys(errors).length === 0,
errors,
};
};
// Usage
const { isValid, errors } = validateForm({
name: 'Jane',
email: 'jane@example.com',
password: 'secure123',
});
Why the good version is better:
- Validation rules are separated into a reusable object, making it easy to add new fields.
- Uses strict checks and returns specific error messages instead of generic alerts.
- Returns a structured result that the UI or API layer can use directly.
- Follows the DRY principle - no repeated code blocks.
Technical interviewers evaluate not just whether your code works, but how well you follow JavaScript coding standards and best practices. Demonstrating clean coding habits during interviews significantly improves your chances of getting hired. For more interview preparation, check out our JavaScript interview questions guide.
What interviewers look for in your JavaScript code:
-
Meaningful variable and function names: Use descriptive names like
getUserByIdinstead of generic names likegetDataor single letters likex. -
Proper use of
const,let, and arrow functions: Interviewers expect you to use modern ES6+ JavaScript syntax. Learn more about these in our ES6 features guide. - Error handling: Handle failures where you have enough context to recover, report, or respond, and handle relevant edge cases.
- Clean code structure: Break large problems into smaller, reusable functions. Each function should do one thing.
- Edge case handling: Check for empty arrays, null values, and invalid inputs before processing.
Example: Clean vs. messy interview solution
// ? Messy approach interviewers dislike
function f(a) {
var r = [];
for (var i = 0; i < a.length; i++) {
if (a[i] % 2 == 0) r.push(a[i] * 2);
}
return r;
}
// ? Clean approach following JavaScript coding guidelines
const getDoubledEvens = (numbers) => {
if (!Array.isArray(numbers)) return [];
return numbers
.filter((num) => num % 2 === 0)
.map((num) => num * 2);
};
Pro tips for coding interviews:
- Think out loud and explain your approach before writing code.
- Start with input validation and edge cases.
- Use built-in array methods like
map,filter, andreduceto demonstrate JavaScript best practices. - Keep your code DRY (Don't Repeat Yourself) and follow the single responsibility principle.
- Add brief comments to explain complex logic, but don't over-comment obvious code.
Interviewers don't just evaluate correctness - they look for clean structure, modern syntax, edge-case handling, and clear naming. Practice writing code as if it's going into production, not just passing test cases.
Asynchronous code is everywhere in JavaScript - fetching data from APIs, reading files, waiting for user input, running timers. If you don't handle it properly, you'll end up with race conditions, unhandled errors, and code that's really hard to follow. Here are the guidelines that will save you a lot of headaches.
-
Use
async/awaitwhen it makes a workflow clearer. Promises with.then()are still appropriate for some APIs and event-driven code, but long sequential chains can become harder to follow.async/awaitoften reads more naturally for sequential work.// Bad - nested .then() chains get hard to follow function getUser() { return fetch('/api/user') .then(res => res.json()) .then(user => { return fetch(`/api/posts/${user.id}`) .then(res => res.json()) .then(posts => { console.log(user.name, posts); }); }); } // Good - async/await is flat and readable async function getUser() { const userRes = await fetch('/api/user'); const user = await userRes.json(); const postsRes = await fetch(`/api/posts/${user.id}`); const posts = await postsRes.json(); console.log(user.name, posts); } -
Handle rejected
awaitoperations at the boundary where you can add useful context or show a recovery state. A localtry...catchis useful when you can recover; otherwise let the error reach a higher-level handler instead of catching and hiding it.async function loadProfile(userId) { try { const response = await fetch(`/api/users/${userId}`); if (!response.ok) { throw new Error(`Server returned ${response.status}`); } const profile = await response.json(); return profile; } catch (error) { console.error('Could not load profile:', error.message); return null; } } -
Run independent async tasks concurrently with
Promise.all()when appropriate. If two operations do not depend on each other, start them before awaiting the combined result. Consider rate limits, resource usage, ordering, and cancellation before starting many operations together.// Bad - these two calls don't depend on each other, but we wait for each one async function loadDashboard() { const user = await fetchUser(); // waits ~300ms const orders = await fetchOrders(); // waits another ~300ms // total: ~600ms } // Good - run both at the same time async function loadDashboard() { const [user, orders] = await Promise.all([ fetchUser(), fetchOrders() ]); // total: ~300ms (whichever is slower) } -
Don't use
asyncif a function doesn't need it. Addingasyncto a function that never usesawaitjust wraps the return value in an unnecessary Promise. Keep things simple.// Bad - async is pointless here, nothing is being awaited async function getUserName(user) { return user.name; } // Good - just a regular function function getUserName(user) { return user.name; } -
Avoid putting
awaitinside loops when you can batch. Awaiting inside aforloop runs each request one at a time. If the requests are independent, batch them.// Bad - sends one request at a time, very slow for 50 users async function loadAllUsers(userIds) { const users = []; for (const id of userIds) { const user = await fetchUser(id); users.push(user); } return users; } // Good - sends all requests at once async function loadAllUsers(userIds) { const users = await Promise.all( userIds.map(id => fetchUser(id)) ); return users; }
.then() chain makes control flow or error handling harder to understand. Refactor to async/await when it improves clarity, not because of a fixed chain length.The DOM (Document Object Model) is how JavaScript talks to the page. But touching the DOM is expensive - every time you change something, the browser might have to recalculate layouts, repaint pixels, and do a bunch of behind-the-scenes work. These guidelines help you keep your DOM code fast and clean.
-
Cache your DOM lookups.
Every time you call
document.getElementById()ordocument.querySelector(), the browser walks through the DOM tree to find that element. If you're using the same element multiple times, grab it once and store it in a variable.// Bad - searches the DOM three separate times document.getElementById('score').textContent = '10'; document.getElementById('score').style.color = 'green'; document.getElementById('score').classList.add('highlight'); // Good - one lookup, three uses const scoreEl = document.getElementById('score'); scoreEl.textContent = '10'; scoreEl.style.color = 'green'; scoreEl.classList.add('highlight'); -
Use event delegation instead of attaching listeners to every element.
If you have a list with 100 items, don't attach 100 click listeners. Attach one listener to the parent and let events bubble up. This uses less memory and works automatically for items added later.
// Bad - one listener per button (imagine 200 of these) document.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', () => { btn.closest('.item').remove(); }); }); // Good - one listener on the parent handles all buttons document.getElementById('item-list').addEventListener('click', (e) => { if (e.target.classList.contains('delete-btn')) { e.target.closest('.item').remove(); } }); -
Batch your DOM changes.
If you need to add a bunch of elements to the page, don't insert them one by one. Build them all first using a
DocumentFragmentor build the HTML string, then insert once. One big update is way faster than many small ones.// Bad - triggers a reflow for every single item const listWithoutFragment = document.getElementById('todo-list'); tasks.forEach(task => { const li = document.createElement('li'); li.textContent = task; listWithoutFragment.appendChild(li); // browser recalculates layout each time }); // Good - build everything first, insert once const list = document.getElementById('todo-list'); const fragment = document.createDocumentFragment(); tasks.forEach(task => { const li = document.createElement('li'); li.textContent = task; fragment.appendChild(li); }); list.appendChild(fragment); // one single DOM update -
Use
textContentinstead ofinnerHTMLfor plain text.innerHTMLparses HTML, which is slower and opens the door to XSS attacks if you're inserting user input. For plain text,textContentis safer and faster.// Bad - innerHTML parses HTML and is vulnerable to XSS messageEl.innerHTML = userInput; // Good - textContent treats everything as plain text messageEl.textContent = userInput; -
Clean up event listeners when elements are removed.
If you remove an element but forget to remove its event listeners, those listeners stay in memory. Over time this causes memory leaks, especially in single-page apps.
// Set up function handleClick() { console.log('clicked'); } button.addEventListener('click', handleClick); // When removing the element, clean up the listener first button.removeEventListener('click', handleClick); button.remove();
The DOM is a shared resource - the less you touch it, the faster your page runs. Cache elements, batch changes, and use event delegation whenever you can.
The Errors section above covers the basics of try-catch. This section goes further - it's about building error handling into your app's architecture so problems are caught early, reported clearly, and don't crash things for the user.
-
Create custom error classes for different failure types.
When your app has different kinds of errors (validation errors, network errors, auth errors), a generic
Errordoesn't tell you much. Custom error classes let you handle each type differently.class ValidationError extends Error { constructor(field, message) { super(message); this.name = 'ValidationError'; this.field = field; } } class NetworkError extends Error { constructor(statusCode, message) { super(message); this.name = 'NetworkError'; this.statusCode = statusCode; } } // Now you can handle them differently async function handleSubmit(data) { try { await submitForm(data); } catch (error) { if (error instanceof ValidationError) { showFieldError(error.field, error.message); } else if (error instanceof NetworkError) { showToast('Connection problem. Please try again.'); } else { console.error('Unexpected error:', error); } } } -
Show user-friendly messages, log technical details.
Your users don't need to see "TypeError: Cannot read property 'name' of undefined". Show them something helpful. But still log the full error for your team to debug.
async function saveSettings(settings) { try { await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings) }); showToast('Settings saved!'); } catch (error) { // User sees a helpful message showToast('Could not save settings. Check your connection.'); // Developers see the real error in logs console.error('Settings save failed:', error); } } -
Use a centralized error handler for repeated patterns.
If every API call needs the same error handling, don't copy-paste the same try-catch everywhere. Write one wrapper function.
// A reusable wrapper for API calls async function apiCall(url, options = {}) { try { const response = await fetch(url, options); if (!response.ok) { throw new NetworkError(response.status, `Request failed: ${url}`); } return await response.json(); } catch (error) { if (error instanceof NetworkError) { throw error; // re-throw known errors } throw new NetworkError(0, `Network error: ${error.message}`); } } // Now every API call is clean async function loadData() { const user = await apiCall('/api/user'); const posts = await apiCall('/api/posts'); return { user, posts }; } -
Always handle Promise rejections.
An unhandled promise rejection can crash your Node.js process or silently fail in the browser. Every Promise should have a
.catch()or be inside atry...catch.// Bad - if this fails, the error vanishes silently fetch('/api/data').then(res => res.json()); // Good - always handle the rejection fetch('/api/data') .then(res => res.json()) .catch(err => console.error('Failed to load data:', err)); -
Validate inputs at the boundary, not deep inside your code.
Check for bad data as early as possible - right when it enters your function. Don't let invalid values travel through multiple layers before something breaks.
// Good - validate right at the start function createUser(name, email) { if (!name?.trim()) { throw new ValidationError('name', 'Name cannot be empty'); } if (!email?.includes('@')) { throw new ValidationError('email', 'Invalid email address'); } // Safe to proceed - we know the data is valid return { name: name.trim(), email: email.toLowerCase() }; }
JavaScript runs on the client side, which means users (and attackers) can see and manipulate your code. You can't trust anything that comes from the browser - user input, URL parameters, cookies, all of it. These guidelines help protect your app and your users.
-
Never insert user input directly into HTML.
This is the number one cause of Cross-Site Scripting (XSS) attacks. If a user types
<script>alert('hacked')</script>into a form and you dump that intoinnerHTML, their script runs on every visitor's browser.// Bad - XSS vulnerability: user input runs as HTML const comment = userInput; commentBox.innerHTML = comment; // Good - textContent escapes everything automatically commentBox.textContent = comment; // Also good - if you need HTML structure, sanitize first function sanitize(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } commentBox.innerHTML = `<p>${sanitize(comment)}</p>`; -
Never use
eval()ornew Function()with user input. These execute arbitrary strings as code. If any part of that string comes from a user, an attacker can run whatever JavaScript they want in your app.// Bad - executing user-controlled strings const userFormula = getUserInput(); const result = eval(userFormula); // attacker can run anything here // Good - use a safe parser or whitelist of operations const allowedOps = { '+': (a, b) => a + b, '-': (a, b) => a - b }; function calculate(a, op, b) { if (!allowedOps[op]) throw new Error('Invalid operation'); return allowedOps[op](a, b); } -
Don't store sensitive data in localStorage or sessionStorage.
These APIs have no access control - any JavaScript on the page can read them, including third-party scripts or injected code. Passwords, tokens with long lifespans, and personal data should never live here.
// Bad - any script on the page can steal this localStorage.setItem('authToken', 'eyJhbGciOiJ...'); localStorage.setItem('password', userPassword); // Good - use httpOnly cookies for auth tokens (set by the server) // For non-sensitive preferences, localStorage is fine localStorage.setItem('theme', 'dark'); localStorage.setItem('language', 'en'); -
Validate and sanitize on the server too.
Client-side validation is for user experience - it gives instant feedback. But anyone can bypass it by opening the browser console. The server must always validate and sanitize data before using it.
// Client side - for quick feedback function validateAge(age) { if (age < 0 || age > 150) { showError('Please enter a valid age'); return false; } return true; } // Server side - the real protection (someone can skip client validation) app.post('/api/register', (req, res) => { const age = Number(req.body.age); if (!Number.isInteger(age) || age < 0 || age > 150) { return res.status(400).json({ error: 'Invalid age' }); } // proceed with registration }); -
Be careful with third-party libraries.
Every npm package you install can run code in your app. Use well-maintained packages with good reputations. Run
npm auditregularly. Remove packages you're not using anymore.
Treat all user input as hostile. Never trust data from the browser. Escape output, validate input, and keep sensitive data out of client-side storage.
Fast code matters. Users leave slow pages, and performance issues compound as your app grows. Most JavaScript performance problems come from doing too much work, doing work too often, or doing it at the wrong time. Here's how to avoid the common traps.
-
Debounce expensive operations triggered by user input.
Events like
scroll,resize, andinputfire dozens of times per second. If your handler does heavy work (like API calls or DOM updates), use a debounce to limit how often it actually runs.// A simple debounce function function debounce(fn, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } // Bad - fires an API call on every single keystroke searchInput.addEventListener('input', () => { fetch(`/api/search?q=${searchInput.value}`); }); // Good - waits until the user stops typing for 300ms searchInput.addEventListener('input', debounce(() => { fetch(`/api/search?q=${searchInput.value}`); }, 300)); -
Avoid unnecessary object creation in loops.
Creating new arrays, objects, or functions inside a loop that runs thousands of times puts pressure on garbage collection and slows things down.
// Bad - creates a new regex object every iteration function findMatches(items) { return items.filter(item => { const pattern = new RegExp('active', 'i'); // new object each time! return pattern.test(item.status); }); } // Good - create it once outside the loop function findMatches(items) { const pattern = /active/i; return items.filter(item => pattern.test(item.status)); } -
Use
Mapfor frequent lookups instead of scanning arrays. If you're constantly checking "does this item exist?" or "get me the item with this ID", an array with.find()gets slower as the list grows. AMapgives you instant lookups.// Bad - scans the whole array every time (slow for large lists) const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, /* ... */]; function getUser(id) { return users.find(u => u.id === id); // O(n) - gets slower as list grows } // Good - instant lookup with a Map const userMap = new Map(users.map(u => [u.id, u])); function getUser(id) { return userMap.get(id); // O(1) - same speed regardless of size } -
Lazy load things the user might not need right away.
Don't load a heavy charting library on page load if the chart is hidden behind a tab. Load it when the user actually needs it.
// Bad - loads the chart library immediately, even if user never opens the tab import { renderChart } from './heavy-chart-library.js'; // Good - loads only when the user clicks the "Analytics" tab document.getElementById('analytics-tab').addEventListener('click', async () => { const { renderChart } = await import('./heavy-chart-library.js'); renderChart(data); }); -
Watch out for memory leaks.
The three most common causes of memory leaks in JavaScript: event listeners that are never removed, timers (
setInterval) that are never cleared, and closures that hold references to large objects. Always clean up after yourself.// Bad - interval runs forever, even after the component is gone setInterval(() => { updateClock(); }, 1000); // Good - store the ID and clear it when done const clockTimer = setInterval(() => { updateClock(); }, 1000); // Later, when the clock is removed from the page clearInterval(clockTimer);
JavaScript coding guidelines are a set of conventions, rules, and best practices that developers follow to write clean, readable, and maintainable code. They are important because they ensure consistency across a codebase, reduce bugs, make code reviews faster, and help new team members understand existing code quickly. In professional environments, following a JavaScript style guide is considered essential for building scalable applications that can be maintained over time by different developers.
JavaScript coding standards refer to the broader set of rules covering code quality, security, performance, and correctness. A JavaScript style guide focuses more specifically on formatting, naming conventions, indentation, and code appearance. In practice, the terms are often used interchangeably. Popular JavaScript style guides like Airbnb, Google, and StandardJS combine both coding standards and style rules into a single comprehensive guide. Both are essential parts of JavaScript best practices.
When every developer on a team follows the same JavaScript coding guidelines, the entire codebase looks like it was written by a single person. This consistency reduces confusion during code reviews, makes pair programming smoother, and decreases the time needed to understand unfamiliar parts of the codebase. Tools like ESLint and Prettier can automatically enforce JavaScript coding standards, ensuring that all team members adhere to the same rules without manual effort.
The most common JavaScript coding mistakes include using var instead of
const or let, using loose equality
(==) instead of strict equality (===),
not handling asynchronous errors with try...catch, polluting the global scope
with variables, mutating function arguments instead of returning new values, and not using modern ES6+ features
like arrow functions, template literals, and destructuring. Following JavaScript coding guidelines helps
beginners avoid these pitfalls from the start.
To prepare JavaScript coding standards for interviews, practice writing clean code with meaningful variable names,
proper error handling, and modern ES6+ syntax. Familiarize yourself with common JavaScript best practices like
using const by default, preferring array methods over loops, handling edge
cases, and following the single responsibility principle. Review popular JavaScript style guides like Airbnb's
guide. During interviews, demonstrate that you think about code quality, not just getting the correct output.
Interviewers notice clean formatting, proper scoping, and well-structured solutions.