JavaScript Code Review Checklist: All Steps Included

We all strive to build high-quality, performant, and secure software. Whether you’re gearing up for a crucial investment round or aiming to accelerate your development cycle, a robust codebase is an absolute must-have. But how can you ensure that your team is consistently delivering exceptional code? The answer lies within an effective code review.

In this post, we’re going over the essentials of developing robust JavaScript apps and code review best practices. By following our JavaScript code review checklist, you can reduce the time spent on bug fixes, improve your team’s efficiency, and ultimately deliver better products faster. In this article, we’ll cover:

Consistent Code Styling and Formatting

By focusing on consistent code styling and formatting, you’re establishing a solid foundation for a healthy codebase. A uniform codebase not only looks professional, but also makes it easier for your team to collaborate, spot errors, and onboard new developers. Setting up style guides and automation tools might take a bit of effort upfront, but the payoff in improved efficiency and code quality is well worth it.

Style Guide & Automation Tools:

  • Choose an established style guide to follow, like Airbnb’s, Google’s, or JavaScript Standard style guides
  • If needed, customize it to better suit your project’s specific requirements
  • Adhere to the style guide to ensure that every piece of code in your project looks and feels the same, regardless of who wrote it
  • To ensure consistent formatting across your codebase, integrate a code formatter tool like Prettier
  • Prettier can automatically format your code to match the chosen style guide, freeing your developers from the burden of manual formatting
// ESLint configuration with Airbnb style guide and Prettier
module.exports = {
  env: {
    browser: true,
    es2021: true
  },
  extends: [
    'airbnb-base',
    'prettier'
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module'
  },
  rules: {
    'no-console': 'warn',
    'import/prefer-default-export': 'off'
  }
};


// Code formatted with Prettier
// Before
function calculateTotal(price,quantity,tax)
{
    let total=price*quantity;
    total+=total*tax;
    return total;
}

// After
function calculateTotal(price, quantity, tax) {
  let total = price * quantity;
  total += total * tax;
  return total;
}

Clear Naming Conventions:

Use names that clearly indicate what the variable or function represents. For example, use getUserData() instead of getData()
In JavaScript, it’s common to use camelCase for variables and functions, and PascalCase for classes
Steer clear of abbreviations: While they might save a few keystrokes, abbreviations can be confusing
Use the same terms across your codebase. If you use fetch in one place, don’t use retrieve in another for the same action

Bad Practice:

function calc(n) {
  // complex calculation
}
Good Practice:
function calculateTotalPrice(numberOfItems) {
  // complex calculation
}

Code Readability and Maintainability

Clean code reduces the cognitive load on developers, minimizes errors, and accelerates development cycles. Clean code is the practice of writing code in a way that others (and your future self) can easily understand, maintain, and build upon.

Modular Code Structure:

  • Use descriptive file and function names that convey their purpose
  • Group related functions into logical folders or modules (e.g., utils, services, controllers)
  • Leverage object-oriented or functional programming, depending on your team’s preference and the project’s needs

Bad Practice:

// Bad: A single function handling multiple responsibilities
function handleUserRequest(req, res) {
  // Validate request
  if (!req.userId) {
    res.status(400).send('User ID is required');
    return;
  }

  // Fetch user data
  database.query(`SELECT * FROM users WHERE id = ${req.userId}`, (err, user) => {
    if (err) throw err;

    // Process user data
    const processedData = processData(user);

    // Send response
    res.send(processedData);
  });
}
Good Practice:
// Good: Separate functions for each responsibility

function validateRequest(req) {
  if (!req.userId) {
    throw new Error('User ID is required');
  }
}

function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    database.query(`SELECT * FROM users WHERE id = ${userId}`, (err, user) => {
      if (err) reject(err);
      resolve(user);
    });
  });
}

function processUserData(user) {
  // Process and return user data
  return {
    id: user.id,
    name: user.name.toUpperCase(),
    // more processing...
  };
}

async function handleUserRequest(req, res) {
  try {
    validateRequest(req);
    const user = await fetchUserData(req.userId);
    const data = processUserData(user);
    res.send(data);
  } catch (error) {
    res.status(400).send(error.message);
  }
}

Deep Nesting Prevention:

  • Use control flow libraries: Libraries like async can help manage asynchronous code
  • Refactor complex conditions: Break down complex if statements into smaller functions or use guard clauses
  • Employ early returns: Exit a function early if certain conditions are met to avoid additional nesting

Bad Practice:

// Bad: Deeply nested if-statements reduce readability and increase complexity
if (user) {
  if (user.isActive) {
    if (user.hasPermission) {
      // Execute action
    } else {
      // No permission
    }
  } else {
    // User not active
  }
} else {
  // No user found
}
Good Practice:
// Good: Using early returns to simplify logic and improve readability
if (!user) {
  // No user found
  return;
}
if (!user.isActive) {
  // User not active
  return;
}
if (!user.hasPermission) {
  // No permission
  return;
}
// Execute action

Comments and Documentation:

  • Document functions and classes: Use comments to explain the purpose, parameters, and return values
  • Explain complex logic: If you had to think hard about it, comment on it
  • Keep comments up-to-date: Review comments during code reviews and updates
  • Avoid commenting out code: Remove unused code instead of commenting it out; version control has your back
/**
 * Calculates the final price by applying tax and discount.
 * @param {Object} params - Calculation parameters
 * @param {number} params.basePrice - Base price before calculations
 * @param {number} params.taxRate - Tax rate as a decimal (e.g., 0.2 for 20% tax)
 * @param {number} params.discount - Discount amount to apply
 * @returns {number} Final price after calculations
 * @throws {ValidationError} If any parameters are invalid
 */
function calculateFinalPrice({ basePrice, taxRate, discount }) {
  // Input validation
  if (!isValidPrice(basePrice)) {
    throw new ValidationError('Invalid base price');
  }

  // Calculate tax
  const taxAmount = basePrice * taxRate;

  // Apply discount
  const discountedPrice = basePrice + taxAmount - discount;

  // Ensure final price is not negative
  return Math.max(0, discountedPrice);
}

// Usage example
try {
  const finalPrice = calculateFinalPrice({
    basePrice: 100,
    taxRate: 0.2,
    discount: 25
  });
  console.log(`Final price: $${finalPrice}`);
} catch (error) {
  logger.error('Price calculation failed:', error);
}

Efficient Error Handling

Errors are inevitable in software development. How you approach handling these errors is what makes the difference between a nice user experience and a frustrating crash. With efficient error handling, you’re proactively strengthening your app’s resilience. This also helps developers debug and maintain your code.

Use of Try-Catch Blocks:

  • Use sparingly: Only wrap code that can throw exceptions, not entire functions or modules
  • Asynchronous code: Remember that try-catch won’t catch errors in asynchronous code (e.g., inside setTimeout or Promise callbacks). Use .catch() for Promises or handle errors in async functions
  • Don’t suppress errors: Catch errors to handle them appropriately, not to hide them

Bad Practice:

// Bad: Not handling potential errors
function parseJSON(jsonString) {
  return JSON.parse(jsonString);
}

const data = parseJSON(userInput);
console.log(data.name);
Good Practice:
// Good: Handling errors with try-catch
function parseJSON(jsonString) {
  try {
    return JSON.parse(jsonString);
  } catch (error) {
    console.error('Failed to parse JSON:', error);
    return null;
  }
}

const data = parseJSON(userInput);
if (data) {
  console.log(data.name);
} else {
  console.log('Invalid input provided.');
}

Custom Error Messages:

  • Provide custom error messages to make debugging easier and improve the user experience
  • Clearly state the nature of the error
class ValidationError extends Error {
  constructor(message, { cause, ...extra } = {}) {
    super(message);
    this.name = 'ValidationError';
    this.cause = cause;
    this.extra = extra;
  }
}

function calculateDiscount(price, discountPercentage) {
  if (price <= 0) {
    throw new ValidationError('Price must be a positive number', {
      cause: 'Invalid price',
      price,
    });
  }

  if (discountPercentage < 0 || discountPercentage > 1) {
    throw new ValidationError('Discount percentage must be between 0 and 1', {
      cause: 'Invalid discount percentage',
      discountPercentage,
    });
  }

  return price * (1 - discountPercentage);
}

try {
  const discountedPrice = calculateDiscount(-100, 0.2);
  console.log(`Discounted price: $${discountedPrice}`);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(`${error.message} (${error.cause})`);
    console.error(error.extra);
  } else {
    console.error('An unexpected error occurred:', error);
  }
}

Logging:

  • Log in a structured format (e.g., JSON) for easier parsing and analysis
  • Be cautious not to log passwords, tokens, or personal user information
  • Use appropriate log levels: Error (for serious issues that need immediate attention), Warn (for potential problems or important notices), Info (general operational entries about what’s going on), Debug (detailed information used for debugging)
  • Use log management systems like ELK Stack (Elasticsearch, Logstash, Kibana) or cloud services to aggregate and analyze logs

Bad Practice:

// Bad: Using console.log in production code
function fetchData(url) {
  fetch(url)
    .then((response) => response.json())
    .catch((error) => console.log('Error fetching data:', error));
}
Good Practice:
// Good: Using a logging library
const logger = require('your-preferred-logger');

function fetchData(url) {
  fetch(url)
    .then((response) => response.json())
    .catch((error) => {
      logger.error('Error fetching data:', {
        message: error.message,
        stack: error.stack,
        url,
      });
    });
}

Performance Optimization

Users expect that apps will be lightning-fast and responsive. So you may wonder, how fast should a website load in 2024? According to Google, anything above 2.5 seconds needs improvement, and anything below 4 seconds is considered poor page-speed. Performance optimization is the key to keeping your users happy and engaged. Here are some trusted strategies for optimizing performance throughout your JavaScript applications.

Efficient Data Manipulation:

  • Select appropriate data structures:
    • Arrays are ideal for ordered collections, and when you need to perform operations like map, filter, or reduce
    • Objects (or Maps) are better for key-value pairs with quick lookup times
    • Use Set for unique values and fast existence checks
    • Use Map for key-value pairs where keys can be of any type
  • Minimize data processing: Only process data that’s necessary; use pagination or lazy loading for large datasets
  • Avoid deep copying objects unnecessarily: Deep copying can be expensive; use shallow copies when possible or immutable data structures
  • Use web workers: Offload heavy computations to web workers to prevent blocking the main thread

Bad Practice:

// Bad: Using an array to check for the existence of a value
const items = ['apple', 'banana', 'orange'];

function hasItem(item) {
  return items.indexOf(item) !== -1;
}
Good Practice:
// Good: Using a Set for efficient existence checks
const itemsSet = new Set(['apple', 'banana', 'orange']);

function hasItem(item) {
  return itemsSet.has(item);
}

Asynchronous Programming:

  • Unless compatibility with older environments is a concern, use async/await for cleaner code
  • Always use try/catch with async functions to handle errors
  • Stick to one asynchronous pattern in a given block of code to maintain consistency

Bad Practice:

// Bad Practice: Promise Hell, avoid this pattern at any cost
function getUserData(userId) {
  fetchUser(userId)
    .then(user => {
      fetchUserPosts(user.id)
        .then(posts => {
          fetchPostComments(posts[0].id)
            .then(comments => {
              console.log(comments);
            })
            .catch(err => console.error(err));
        })
        .catch(err => console.error(err));
    })
    .catch(err => console.error(err));
}
Good Practice:
// Good Practice: Promise Chaining
function getUserData(userId) {
  return fetchUser(userId)
    .then(user => fetchUserPosts(user.id))
    .then(posts => fetchPostComments(posts[0].id))
    .then(comments => comments)
    .catch(err => {
      logger.error('Failed to fetch user data:', err);
      throw err;
    });
}

// Better Practice: Async/Await with Error Handling
async function getUserData(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetchUserPosts(user.id);
    const comments = await fetchPostComments(posts[0].id);
    return comments;
  } catch (error) {
    logger.error('Failed to fetch user data:', error);
    throw error;
  }
}

// Best Practice: Parallel Operations with Error Handling
async function getUserProfile(userId) {
  try {
    const [user, posts, settings] = await Promise.all([
      fetchUser(userId),
      fetchUserPosts(userId),
      fetchUserSettings(userId)
    ]);
    
    return {
      ...user,
      posts,
      settings
    };
  } catch (error) {
    logger.error('Failed to fetch user profile:', error);
    throw error;
  }
}

Memory Leak Prevention:

  • Remove unused event listeners and timers: Clean up after yourself to prevent unintended memory retention
  • Avoid unnecessary global variables: Keep variables scoped to where they are needed
  • Manage references carefully: Be mindful of how closures and callbacks hold onto variables
  • Regularly test and profile: Use tools to detect and fix memory leaks before they become a problem

Bad Practice:

// Bad: Not removing event listeners when elements are removed
function attachListener() {
  const button = document.getElementById('myButton');
  button.addEventListener('click', handleClick);
}

function handleClick() {
  // Handle click event
}

// Later, the button is removed from the DOM
document.getElementById('myButton').remove();
Good Practice:
// Good: Properly removing event listeners and managing references
function attachListener() {
  const button = document.getElementById('myButton');
  const handleClick = () => {
    // Handle click event
    console.log('Button clicked!');
  };

  button.addEventListener('click', handleClick);

  // Add a cleanup function to remove the listener when the button is removed
  const cleanup = () => {
    button.removeEventListener('click', handleClick);
    button.remove();
  };

  return cleanup;
}

// Example usage
const cleanupFunction = attachListener();

// Later, when the button is no longer needed
cleanupFunction();

Security Best Practices

User input is one of the most significant vulnerabilities in any application. Without proper sanitization, your app becomes a playground for attackers looking to exploit weaknesses through Cross-Site Scripting (XSS) and injection attacks. Securing data during transmission & storage and regularly checking dependencies is a must.

Sanitizing User Input:

  • Use trusted libraries like DOMPurify for sanitizing HTML and preventing XSS attacks
  • Validate input server-side: Client-side validation can be bypassed; always validate on the server
  • Allowlist Input: Accept only expected data; define acceptable patterns and reject everything else
  • Escape output appropriately: Use different escaping methods for HTML, URLs, JavaScript, and CSS
// Using 3rd-party library DOMPurify as it is more secure and robust than manually sanitizing the input using string manipulation or regular expressions
import DOMPurify from 'dompurify';

function sanitizeUserInput(unsafeInput) {
  // Sanitize HTML input to prevent XSS attacks
  const sanitizedInput = DOMPurify.sanitize(unsafeInput, {
    ALLOWED_TAGS: ['a', 'b', 'i', 'em', 'strong'],
    ALLOWED_ATTR: ['href'],
  });

  return sanitizedInput;
}

// Example usage
const userInput = 'Click me';
const cleanInput = sanitizeUserInput(userInput);

console.log(cleanInput); 
// Output: Click me

Using HTTPS and Secure Cookies:

  • Set secure cookie attributes: Secure Flag (ensures cookies are only sent over HTTPS), HttpOnly Flag (prevents JavaScript from accessing cookies, mitigating XSS attacks), SameSite Attribute (protects against CSRF attacks)
  • Regularly update certificates: Monitor certificate validity and renew before expiration
  • Disable weak protocols and ciphers: Use strong encryption standards like TLS 1.3+

Dependency Management:

  • Lock dependency versions: Use package-lock.json or yarn.lock to ensure consistent installs across environments
  • Regularly update dependencies: Schedule time to update packages and test for compatibility
  • Use npm audit or yarn audit commands regularly to check for packages vulnerabilities
  • Remove unused dependencies: Lean codebase reduces potential attack surface and improves performance
  • Prefer official sources: Double-check package names to prevent installing malicious packages with similar names
  • Automate checks: Integrate tools that scan dependencies during build processes

Comprehensive Testing

Comprehensive testing spans multiple levels, from testing individual units of code to verifying that entire systems work together well. Ongoing testing ensures you’re building a solid foundation for your app’s future growth and success. Unit testing, integration testing, and achieving high test coverage can noticeably elevate the quality of your JavaScript apps.

Unit Testing:

  • Test single responsibility: Each test should focus on one aspect of the function’s behavior
  • Use descriptive test names: Clearly state what the test is verifying
  • Mock external dependencies: Isolate the unit of code by mocking APIs, databases, or other services
  • Aim for deterministic tests: Tests should produce the same results every time they run

Bad Practice:

// Function to be tested
function add(a, b) {
  return a + b;
}

// Test suite
describe('add function', () => {
  it('should add two numbers', () => {
    assert.equal(add(2, 3), 5);
  });
});
Good Practice:
// Function to be tested
function add(a, b) {
  return a + b;
}

// Test suite
describe('add function', () => {
  it('should add two positive numbers', () => {
    assert.equal(add(2, 3), 5);
  });

  it('should add two negative numbers', () => {
    assert.equal(add(-2, -3), -5);
  });

  it('should handle a positive and a negative number', () => {
    assert.equal(add(2, -3), -1);
  });

  it('should handle zero as an argument', () => {
    assert.equal(add(2, 0), 2);
    assert.equal(add(0, 3), 3);
  });
});

Integration and End-to-End Testing:

  • Maintain a test environment: Use a separate environment that mirrors production settings
  • Use test data: Populate databases with known data for consistent results
  • Automate testing: Integrate tests into your CI/CD pipeline to run on every build or deployment
  • Prioritize critical paths: Focus on the most important user flows and integration points

Test Coverage:

  • Aim for a high coverage percentage (e.g., 80-90%) but understand that 100% coverage doesn’t guarantee bug-free code
  • Regularly check which parts of your code aren’t covered and assess whether they need tests
  • Exclude code that’s auto-generated or not critical (e.g., configuration files) from coverage reports
  • Focus on meaningful tests that validate behavior: Writing superficial tests to increase coverage percentages is a counterproductive practice

Proper Use of Version Control

Version control allows multiple developers to work on the same codebase without stepping on each other’s toes. Proper use of version control enhances collaboration, ensures code quality, and supports the agile development process. By adopting effective branching strategies, writing meaningful commit messages, and engaging in thorough code reviews, you set your team up for success.

Branching:

  • Keep branches short-lived: Regularly merge changes to prevent diverging codebases
  • Use descriptive branch names: Include the type and purpose, e.g., feature/add-payment-method or bugfix/fix-login-error
  • Branch protection rules: Prevent direct pushes to main or master and require PR reviews

Commit Messages:

  • Use the imperative mood: Use verbs like “Add,” “Fix,” “Update”
  • Keep subject lines concise: Summarize the change in 50 characters or less
  • Provide context: Help others understand the reasoning behind changes
  • Separate changes appropriately: One logical change per commit

Bad Practice:

git commit -m "Fix stuff
Good Practice:
git commit -m "Fix authentication error when user logs in with Google"

Pull Requests:

  • When opening a PR, provide a descriptive title and description
  • Mention any dependencies or related PRs
  • Assign a specific team member as a reviewer
  • Use merge strategies appropriately: Merge Commit (keeps all commits and history), Squash and Merge (combines commits into one for a cleaner history); Rebase and Merge (applies your commits on top of the main branch for a linear history)

Dependency Management

Dependencies can significantly impact your app’s stability, performance, and security. Effective dependency management is a balancing act between leveraging third-party code and maintaining control over your project’s integrity and performance. Here are some best practices to help you avoid the so-called “dependency hell.”

Lock File Usage:

  • Always commit lock files: Keep them under version control to maintain consistency
  • Don’t edit lock files manually: Let the package manager handle them to avoid corruption
  • Update lock files intentionally: Run npm install or yarn install without changing versions unless you intend to update
  • Handling lock files in applications: Commit lock files to ensure deployment consistency
  • Handling lock files in libraries/packages: Consider not committing lock files, allowing the end application to control dependency versions

Avoiding Dependency Hell:

  • Understand how Caret (^) and Tilde (~) Operators affect version ranges
  • Caret (^1.2.3): Updates to the latest minor version (but not major)
  • Tilde (~1.2.3): Updates to the latest patch version (but not minor or major)
  • Review changelogs before updating: Check for breaking changes, especially when updating major versions
  • Automate dependency management: Use tools like Dependabot or Renovate to automate updates and create pull requests for them
  • Test thoroughly after updates: Run your test suite to catch any issues introduced by updated dependencies

Modular Dependencies:

  • Import only necessary parts: Smaller bundles load faster, improving user experience
  • Use bundlers like Webpack or Rollup that support tree-shaking to eliminate unused code
  • Ensure you’re using ES6 modules (import/export) which are statically analyzable

Additional Considerations:

  • Dynamically import modules when needed to improve initial load times
  • Ensure that multiple versions of the same library aren’t included; use tools like npm dedupe or yarn-deduplicate

Accessibility Considerations

The purpose of accessibility is to make your application usable by as many people as possible, including those with disabilities. By incorporating accessibility best practices, you not only comply with legal requirements in some regions, but also provide a better user experience for everyone.

Semantic HTML:

  • Don’t overuse <div> and <span> when a semantic element is more appropriate
  • Don’t put <div> inside <p>
  • Ensure heading levels are used in order (e.g., <h2> follows <h1>)
  • Use strict hierarchy of <h1> to <h6> to define headings and subheadings
  • Use <nav> to enclose navigation links
  • Use <main> to wrap the main content of the page
  • Use <section> and <article> for distinct parts of content
  • Use appropriate form controls like <label>, <input>, <select>, and <textarea>

ARIA Roles and Attributes:

  • Use W3C WAI-ARIA specification for implementing best accessibility recommendations
  • Use ARIA as a last resort: Prefer native HTML elements before adding ARIA roles
  • Ensure proper keyboard interaction: Custom widgets should support keyboard navigation and activation
  • Keep ARIA attributes updated: Dynamically update ARIA states to reflect changes
  • Define the type of widget assigning appropriate role corresponding to its place in roles hierarchy (role=”button”, role=”dialog”)
  • Describe the current state of elements specific roles (aria-checked, aria-expanded)
  • Provide accessible names (aria-label, aria-labelledby)

Bad Practice:

// Bad: Adding ARIA roles to non-semantic elements unnecessarily
<div role="button" onclick="toggleMenu()">Menu</div>
Good Practice:
// Good: Use a button and enhance with ARIA if needed 
<button aria-expanded="false" aria-controls="menu" onclick="toggleMenu(this)">Menu</button>
<ul id="menu" hidden>
  <!-- Menu items -->
</ul>

Keyboard Navigation:

  • Use standard controls: Native HTML elements like <button>, <a>, <input> are keyboard-friendly by default
  • Manage focus order: Arrange focusable elements in a logical order
  • Use tabindex=”0″ to make custom elements focusable, but use sparingly
  • Ensure that focused elements have a visible outline or highlight
  • For custom components, listen to keyboard events like keydown and respond appropriately
  • Test keyboard accessibility: Use the Tab key to move focus forward and Shift + Tab to move backward
  • Use accessibility testing tools like Axe or Lighthouse

Your JS Partner: Development & Audit Services

We’ve been in the trenches of web development since 2005, partnering with startups and scaleups to bring their visions to life. From building interactive front-end applications with React and Vue.js to developing scalable back-end services with Node.js, we’ve tackled a wide array of challenges that JavaScript projects present.

Our team of seasoned developers, designers, and project managers can strengthen your project and help you complete important tasks faster without feeling overwhelmed. Here’s a glimpse into the specific services we offer and how our clients capitalize on our expertise and resources to achieve their milestones:

Development from Scratch. We specialize in building custom software solutions from the ground up. See how we helped Kooky develop a web-based platform for tracking reusable coffee cups and become the #1 green tech startup in Switzerland.

Code Refactoring. We can help you refactor your app and bring it up to modern standards. Cakemail, an email marketing startup based in Montreal, hired Redwerk to help them refactor a subscription form module. We delivered the refactored code in less than 90 days, allowing Cakemail to launch an essential upgrade on time.

Extending Functionality. Increase the attractiveness of your product by adding new features. Orderstep, a leading sales and CRM platform, leveraged our expertise in front-end and React development to extend their solution with a webshop module for Premium users, thus increasing the company’s revenue.

Code Review: Get an impartial view of your app’s quality with a comprehensive code review performed by Redwerk. We’ve seen firsthand how a fresh pair of eyes can uncover hidden issues or opportunities for optimization that might be overlooked when your team is deep in the day-to-day development grind.

Our hands-on experience means we understand the pressures of tight deadlines and the need for rapid iteration without compromising code quality. Whether you’re looking for a comprehensive code audit, guidance on best practices, or full-fledged development, we’ve got a team of tech experts to support you. Feel free to contact us—we’d love to chat, learn about your business needs, and help you get started on addressing them.

See how we helped Kooky develop a web platform for tracking reusable coffee cups and become #1 green tech startup in Switzerland

Please enter your business email isn′t a business email