Skip to main content

Secure Coding Practices in JavaScript

Created: May 8, 2026 Larry Qu 8 min read

Secure coding practices are fundamental to building safe applications. This article covers error handling, logging, secrets management, and security best practices.

Introduction

Secure coding practices provide:

  • Vulnerability prevention
  • Error handling
  • Information security
  • Code quality
  • Compliance

Understanding these practices helps you:

  • Write secure code
  • Handle errors safely
  • Manage secrets
  • Prevent information leaks
  • Build reliable systems

Error Handling

Safe Error Handling

// ✅ Good: Safe error handling
function processUserData(data) {
  try {
    if (!data || typeof data !== 'object') {
      throw new Error('Invalid data format');
    }

    // Process data
    return { success: true, data };
  } catch (error) {
    // Log error securely (don't expose details to user)
    console.error('Error processing data:', error.message);

    // Return generic error to user
    return {
      success: false,
      error: 'An error occurred while processing your request'
    };
  }
}

// Usage
const result = processUserData({ name: 'John' });
console.log(result); // { success: true, data: { name: 'John' } }

Error Logging

// ✅ Good: Secure error logging
class SecureLogger {
  static log(level, message, context = {}) {
    // Remove sensitive data
    const sanitized = this.sanitizeContext(context);

    const logEntry = {
      timestamp: new Date().toISOString(),
      level,
      message,
      context: sanitized
    };

    // Log to secure location (not console in production)
    if (process.env.NODE_ENV === 'production') {
      this.sendToLoggingService(logEntry);
    } else {
      console.log(JSON.stringify(logEntry));
    }
  }

  static sanitizeContext(context) {
    const sensitiveKeys = ['password', 'token', 'secret', 'apiKey', 'creditCard'];
    const sanitized = { ...context };

    for (const key of sensitiveKeys) {
      if (key in sanitized) {
        sanitized[key] = '[REDACTED]';
      }
    }

    return sanitized;
  }

  static sendToLoggingService(logEntry) {
    // Send to secure logging service (e.g., Sentry, LogRocket)
    // fetch('/api/logs', { method: 'POST', body: JSON.stringify(logEntry) });
  }

  static error(message, error, context = {}) {
    this.log('ERROR', message, { ...context, error: error.message });
  }

  static warn(message, context = {}) {
    this.log('WARN', message, context);
  }

  static info(message, context = {}) {
    this.log('INFO', message, context);
  }
}

// Usage
SecureLogger.error('User login failed', new Error('Invalid credentials'), {
  username: 'john',
  password: 'secret123' // Will be redacted
});

Custom Error Classes

// ✅ Good: Custom error classes
class ApplicationError extends Error {
  constructor(message, statusCode = 500, details = {}) {
    super(message);
    this.name = 'ApplicationError';
    this.statusCode = statusCode;
    this.details = details;
  }

  toJSON() {
    return {
      error: this.message,
      statusCode: this.statusCode
    };
  }
}

class ValidationError extends ApplicationError {
  constructor(message, details = {}) {
    super(message, 400, details);
    this.name = 'ValidationError';
  }
}

class AuthenticationError extends ApplicationError {
  constructor(message = 'Authentication failed') {
    super(message, 401);
    this.name = 'AuthenticationError';
  }
}

class AuthorizationError extends ApplicationError {
  constructor(message = 'Access denied') {
    super(message, 403);
    this.name = 'AuthorizationError';
  }
}

// Usage
try {
  throw new ValidationError('Invalid email format', { field: 'email' });
} catch (error) {
  console.log(error.toJSON());
  // { error: 'Invalid email format', statusCode: 400 }
}

Secrets Management

Environment Variables

// ✅ Good: Use environment variables for secrets
// .env file (never commit to version control)
// DATABASE_URL=postgresql://user:password@localhost/db
// API_KEY=secret_key_here
// JWT_SECRET=jwt_secret_here

// Load environment variables
require('dotenv').config();

const dbURL = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;
const jwtSecret = process.env.JWT_SECRET;

// Validate required secrets
function validateSecrets() {
  const required = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'];

  for (const secret of required) {
    if (!process.env[secret]) {
      throw new Error(`Missing required environment variable: ${secret}`);
    }
  }
}

validateSecrets();

Secrets Vault

// ✅ Good: Use secrets vault (e.g., HashiCorp Vault, AWS Secrets Manager)
class SecretsManager {
  constructor() {
    this.cache = new Map();
    this.cacheTTL = 3600000; // 1 hour
  }

  async getSecret(secretName) {
    // Check cache
    const cached = this.cache.get(secretName);
    if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
      return cached.value;
    }

    // Fetch from vault
    const secret = await this.fetchFromVault(secretName);

    // Cache result
    this.cache.set(secretName, {
      value: secret,
      timestamp: Date.now()
    });

    return secret;
  }

  async fetchFromVault(secretName) {
    // Example: AWS Secrets Manager
    // const client = new SecretsManagerClient();
    // const response = await client.getSecretValue({ SecretId: secretName });
    // return response.SecretString;

    // Placeholder
    return process.env[secretName];
  }

  clearCache() {
    this.cache.clear();
  }
}

// Usage
const secretsManager = new SecretsManager();
const apiKey = await secretsManager.getSecret('api_key');

Secure Configuration

// ✅ Good: Secure configuration management
class Config {
  static get(key, defaultValue = null) {
    const value = process.env[key];

    if (!value && defaultValue === null) {
      throw new Error(`Configuration key not found: ${key}`);
    }

    return value || defaultValue;
  }

  static getSecret(key) {
    const value = process.env[key];

    if (!value) {
      throw new Error(`Secret not found: ${key}`);
    }

    return value;
  }

  static getBoolean(key, defaultValue = false) {
    const value = process.env[key];
    return value ? value.toLowerCase() === 'true' : defaultValue;
  }

  static getNumber(key, defaultValue = 0) {
    const value = process.env[key];
    return value ? parseInt(value, 10) : defaultValue;
  }
}

// Usage
const dbURL = Config.getSecret('DATABASE_URL');
const port = Config.getNumber('PORT', 3000);
const debug = Config.getBoolean('DEBUG', false);

Dependency Management

Dependency Scanning

// ✅ Good: Scan dependencies for vulnerabilities
// npm audit
// npm audit fix
// npm audit fix --force

// Automated scanning in CI/CD
// npm install -g snyk
// snyk test
// snyk monitor

class DependencyChecker {
  static async checkVulnerabilities() {
    // Use npm audit API
    const { execSync } = require('child_process');

    try {
      const result = execSync('npm audit --json', { encoding: 'utf-8' });
      const audit = JSON.parse(result);

      if (audit.vulnerabilities && Object.keys(audit.vulnerabilities).length > 0) {
        console.warn('Vulnerabilities found:');
        console.warn(JSON.stringify(audit.vulnerabilities, null, 2));
        return false;
      }

      return true;
    } catch (error) {
      console.error('Error checking vulnerabilities:', error.message);
      return false;
    }
  }
}

// Usage
const isSecure = await DependencyChecker.checkVulnerabilities();

Dependency Pinning

// ✅ Good: Pin dependencies to specific versions
// package.json
{
  "dependencies": {
    "express": "4.18.2",
    "lodash": "4.17.21"
  },
  "devDependencies": {
    "jest": "29.5.0"
  }
}

// Use package-lock.json to lock transitive dependencies
// npm ci (instead of npm install) to use locked versions

Input Sanitization

Sanitize User Input

// ✅ Good: Comprehensive input sanitization
class InputSanitizer {
  static sanitize(input, type = 'text') {
    switch (type) {
      case 'text':
        return this.sanitizeText(input);
      case 'email':
        return this.sanitizeEmail(input);
      case 'url':
        return this.sanitizeURL(input);
      case 'number':
        return this.sanitizeNumber(input);
      default:
        return input;
    }
  }

  static sanitizeText(text) {
    return String(text)
      .trim()
      .replace(/[<>]/g, '')
      .substring(0, 1000);
  }

  static sanitizeEmail(email) {
    return String(email)
      .trim()
      .toLowerCase()
      .substring(0, 254);
  }

  static sanitizeURL(url) {
    try {
      const parsed = new URL(url);
      if (!['http:', 'https:'].includes(parsed.protocol)) {
        return null;
      }
      return parsed.toString();
    } catch {
      return null;
    }
  }

  static sanitizeNumber(num) {
    const parsed = parseFloat(num);
    return isNaN(parsed) ? 0 : parsed;
  }
}

// Usage
const email = InputSanitizer.sanitize('  [email protected]  ', 'email');
console.log(email); // '[email protected]'

Rate Limiting

Implement Rate Limiting

// ✅ Good: Rate limiting to prevent abuse
class RateLimiter {
  constructor(maxRequests = 100, windowMs = 60000) {
    this.maxRequests = maxRequests;
    this.windowMs = windowMs;
    this.requests = new Map();
  }

  isAllowed(identifier) {
    const now = Date.now();
    const userRequests = this.requests.get(identifier) || [];

    // Remove old requests outside the window
    const recentRequests = userRequests.filter(
      time => now - time < this.windowMs
    );

    if (recentRequests.length >= this.maxRequests) {
      return false;
    }

    // Add current request
    recentRequests.push(now);
    this.requests.set(identifier, recentRequests);

    return true;
  }

  getRemainingRequests(identifier) {
    const userRequests = this.requests.get(identifier) || [];
    return Math.max(0, this.maxRequests - userRequests.length);
  }
}

// Usage
const limiter = new RateLimiter(10, 60000); // 10 requests per minute

function handleRequest(userId) {
  if (!limiter.isAllowed(userId)) {
    return { error: 'Rate limit exceeded' };
  }

  // Process request
  return { success: true };
}

Security Headers

Set Security Headers

// ✅ Good: Set security headers (server-side)
// Express.js example
const express = require('express');
const app = express();

// Content Security Policy
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' https://fonts.googleapis.com"
  );
  next();
});

// X-Content-Type-Options
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  next();
});

// X-Frame-Options
app.use((req, res, next) => {
  res.setHeader('X-Frame-Options', 'DENY');
  next();
});

// X-XSS-Protection
app.use((req, res, next) => {
  res.setHeader('X-XSS-Protection', '1; mode=block');
  next();
});

// Strict-Transport-Security
app.use((req, res, next) => {
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  next();
});

// Referrer-Policy
app.use((req, res, next) => {
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  next();
});

Practical Security Examples

Secure API Endpoint

// ✅ Good: Secure API endpoint
async function handleUserRequest(req, res) {
  try {
    // 1. Validate input
    const { email, password } = req.body;

    if (!email || !password) {
      throw new ValidationError('Email and password are required');
    }

    // 2. Sanitize input
    const sanitizedEmail = InputSanitizer.sanitize(email, 'email');

    // 3. Check rate limit
    if (!rateLimiter.isAllowed(req.ip)) {
      throw new ApplicationError('Too many requests', 429);
    }

    // 4. Authenticate
    const user = await authenticateUser(sanitizedEmail, password);

    if (!user) {
      throw new AuthenticationError();
    }

    // 5. Authorize
    if (!user.isActive) {
      throw new AuthorizationError('User account is inactive');
    }

    // 6. Process request
    const result = await processUserData(user);

    // 7. Return response
    res.json({ success: true, data: result });
  } catch (error) {
    // 8. Handle error securely
    SecureLogger.error('Request failed', error, { ip: req.ip });

    const statusCode = error.statusCode || 500;
    const message = error.statusCode ? error.message : 'Internal server error';

    res.status(statusCode).json({ error: message });
  }
}

Best Practices

  1. Never expose sensitive information:
    // ❌ Bad
    console.log('Password:', password);
    
    // ✅ Good
    console.log('Authentication attempt');
    ```javascript
    
  2. Use HTTPS everywhere:
    // ✅ Good
    // All communication over HTTPS
    
    // ❌ Bad
    // HTTP connections allowed
    ```javascript
    
  3. Validate and sanitize all input:
    // ✅ Good
    const sanitized = InputSanitizer.sanitize(userInput);
    
    // ❌ Bad
    const data = userInput;
    ```javascript
    

Common Mistakes

  1. Logging sensitive data:
    // ❌ Bad
    console.log('User:', { email, password });
    
    // ✅ Good
    console.log('User login attempt');
    ```javascript
    
  2. Hardcoding secrets:
    // ❌ Bad
    const apiKey = 'sk_live_abc123';
    
    // ✅ Good
    const apiKey = process.env.API_KEY;
    ```javascript
    
  3. Not validating input:
    // ❌ Bad
    const user = await findUser(userId);
    
    // ✅ Good
    if (!userId || typeof userId !== 'number') {
      throw new ValidationError('Invalid user ID');
    }
    const user = await findUser(userId);
    

Summary

Secure coding practices are essential. Key takeaways:

  • Handle errors safely
  • Log securely
  • Manage secrets
  • Scan dependencies
  • Sanitize input
  • Implement rate limiting
  • Set security headers
  • Validate all input

Next Steps

Resources

Comments

Share this article

Scan to read on mobile