Skip to content

JavaScript – Language Features

Cygnus Dynamics targets ES2022+ across all JavaScript projects. Use modern language features — they exist to make code safer, more expressive, and less error-prone.

Variable Declarations

Always use const or let. var is banned.

Keyword When to Use
const Default — any value that does not need reassignment
let When reassignment is genuinely required
var Never — function-scoped, hoisted, source of bugs
// ✅ const by default
const userId = request.params.id;
const orders = await orderService.getOrdersByUser(userId);

// ✅ let only when reassignment is required
let totalAmount = 0;
for (const item of cart.items) {
  totalAmount += item.price * item.quantity;
}

// ❌ var — leaks outside blocks, unreliable
for (var i = 0; i < 10; i++) { }
console.log(i); // 10 — leaked outside the loop

Arrow Functions

Use arrow functions for callbacks and short inline expressions. Use named function declarations for top-level, exported, or standalone functions.

// ✅ Arrow functions for callbacks
const activeUsers = users.filter(user => user.isActive);
const emailAddresses = users.map(user => user.email);
const totalRevenue = orders.reduce((sum, order) => sum + order.total, 0);

// ✅ Named declarations for top-level functions
async function fetchUserProfile(userId) {
  const user = await userRepository.findById(userId);
  return buildProfileResponse(user);
}

// ❌ Anonymous arrow for top-level — anonymous in stack traces
const fetchUserProfile = async (userId) => { ... };

Omit parentheses for single-parameter arrow functions:

// ✅
const names = users.map(user => user.name);
const totals = items.map((item, index) => item.price * quantities[index]);
const getTimestamp = () => Date.now();

Template Literals

Use template literals for interpolation and multi-line strings. Never concatenate with +:

// ✅
const welcomeMessage = `Welcome back, ${user.firstName}!`;
const apiEndpoint = `${API_BASE_URL}/v2/orders/${orderId}`;
const emailBody = `
  Dear ${user.firstName},

  Your order #${order.id} has been confirmed.
  Total: ${formatCurrency(order.total, order.currency)}
`.trim();

// ❌
const welcomeMessage = 'Welcome back, ' + user.firstName + '!';

Destructuring

Object Destructuring

// ✅ Basic destructuring
const { id, email, firstName, role } = user;

// ✅ With renaming
const { id: userId, email: userEmail } = user;

// ✅ With defaults
const { pageSize = 20, pageNumber = 1 } = queryParams;

// ✅ In function parameters — makes the expected shape explicit
function sendWelcomeEmail({ email, firstName, locale = 'en' }) {
  return emailService.send({ to: email, template: `welcome-${locale}` });
}

Array Destructuring

// ✅
const [firstItem, secondItem] = sortedItems;
const [primaryContact, ...otherContacts] = contacts;
const [, secondPlace, thirdPlace] = leaderboard;  // skip elements
[a, b] = [b, a];  // swap without temp variable

Spread and Rest Operators

// ✅ Spread — copy and merge without mutation
const updatedItems = [...cart.items, newItem];
const updatedOrder = { ...existingOrder, status: 'CONFIRMED', updatedAt: new Date() };
const config = { ...defaultConfig, ...userConfig };  // rightmost wins

// ✅ Rest — collect remaining arguments
function logWithPrefix(prefix, ...messages) {
  messages.forEach(msg => console.log(`[${prefix}] ${msg}`));
}

const [first, ...remaining] = items;
const { id, ...otherFields } = user;

Optional Chaining ?.

// ✅ Safe deep access
const city = user?.address?.city;
const firstOrderId = user?.orders?.[0]?.id;
const displayName = user?.profile?.getDisplayName?.();

// ✅ With nullish coalescing for defaults
const city = user?.address?.city ?? 'Unknown';

// ❌ Verbose manual null checks
const city = user && user.address && user.address.city;

Nullish Coalescing ?? and Logical Assignment

Use ?? — not || — for defaults. || triggers on any falsy value including 0, '', and false:

// ✅ ?? — only triggers on null or undefined
const pageSize = request.query.pageSize ?? 20;  // 0 stays 0
const username = config.username ?? 'anonymous';

// ❌ || triggers on ALL falsy values — breaks with 0, '', false
const pageSize = request.query.pageSize || 20;  // pageSize 0 becomes 20 — wrong

Logical Assignment Operators (ES2021)

// ✅ ??= — assign only if null or undefined
config.timeout ??= 5000;
options.retries ??= 3;

// ✅ ||= — assign only if falsy (use carefully — see ?? caveat above)
user.role ||= 'viewer';

// ✅ &&= — assign only if truthy
config.debug &&= process.env.NODE_ENV === 'development';

// These replace verbose patterns like:
config.timeout = config.timeout ?? 5000;  // ← verbose, replaced by ??=

for...of vs forEach vs for Loop

Use for...of for iteration where you need break, continue, or await. Use forEach only for synchronous side effects. Use array methods (map, filter, reduce) for transformations.

// ✅ for...of — supports break, continue, and await
for (const order of orders) {
  if (order.status === OrderStatus.CANCELLED) continue;
  await processOrder(order);
}

// ✅ break works in for...of
for (const item of largeList) {
  if (item.id === targetId) {
    found = item;
    break;  // exits immediately — not possible with forEach
  }
}

// ✅ forEach — synchronous side effects only
users.forEach(user => {
  console.log(`Processing ${user.email}`);
});

// ❌ async inside forEach — errors are silently swallowed
users.forEach(async (user) => {
  await sendEmail(user);  // errors lost — use for...of instead
});

// ❌ for loop for simple iteration — verbose when for...of works
for (let i = 0; i < orders.length; i++) {
  processOrder(orders[i]);  // use for...of: for (const order of orders)
}

// ✅ Traditional for loop — only when you need the index for arithmetic
for (let i = 0; i < pairs.length; i += 2) {
  processPair(pairs[i], pairs[i + 1]);
}

Async / Await

Always use async/await. Avoid raw .then() chains for sequential operations.

// ✅ async/await — reads sequentially
async function getOrderSummary(userId) {
  const user = await userService.getUser(userId);
  const orders = await orderService.getOrdersByUser(userId);
  return { user, orders, totalSpend: calculateTotalSpend(orders) };
}

// ✅ Error handling with try/catch
async function processPayment(orderId, paymentDetails) {
  try {
    const order = await orderService.getOrder(orderId);
    const result = await paymentGateway.charge(order.total, paymentDetails);
    await orderService.markAsPaid(orderId, result.transactionId);
    return result;
  } catch (error) {
    logger.error('Payment failed', { orderId, error: error.message });
    throw new PaymentFailedError(`Payment failed for order ${orderId}`, error);
  }
}

Parallel Execution — Promise.all and Promise.allSettled

// ✅ Promise.all — all must succeed; fails fast on first rejection
const [user, orders, notifications] = await Promise.all([
  userService.getUser(userId),
  orderService.getRecentOrders(userId),
  notificationService.getUnread(userId),
]);

// ✅ Promise.allSettled — when partial failure is acceptable
const results = await Promise.allSettled([
  emailService.send(user.email, orderConfirmation),
  smsService.send(user.phone, orderConfirmation),
  pushService.send(user.deviceToken, orderConfirmation),
]);

// Log failures without aborting — notifications are best-effort
results
  .filter(r => r.status === 'rejected')
  .forEach(r => logger.warn('Notification failed', { reason: r.reason }));

// ❌ Sequential when operations are independent — slower than necessary
const user = await userService.getUser(userId);
const orders = await orderService.getRecentOrders(userId);  // waits for user unnecessarily

Never Mix await and .then()

// ❌ Mixed — confusing control flow
async function fetchData(id) {
  const result = await apiClient.get(id).then(res => res.data);
  return result;
}

// ✅ Pure await
async function fetchData(id) {
  const response = await apiClient.get(id);
  return response.data;
}

AbortController for Cancellable Operations

// ✅ Cancel in-flight fetch requests when component unmounts or user navigates
function createOrderPoller(orderId, onUpdate) {
  const controller = new AbortController();

  const poll = async () => {
    try {
      const response = await fetch(`/api/orders/${orderId}`, {
        signal: controller.signal,
      });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const order = await response.json();
      onUpdate(order);
    } catch (error) {
      if (error.name === 'AbortError') return; // cancelled — not an error
      logger.error('Polling failed', { orderId, error: error.message });
    }
  };

  const interval = setInterval(poll, 5000);

  return () => {
    controller.abort();
    clearInterval(interval);
  };
}

// Usage
const stopPolling = createOrderPoller(orderId, handleOrderUpdate);
// Later: stopPolling();  // cancels the pending request

Object Utilities — Object.entries, Object.values, Object.fromEntries

// ✅ Object.entries — iterate over key-value pairs
const orderMap = { 'ORD-1': order1, 'ORD-2': order2 };

for (const [reference, order] of Object.entries(orderMap)) {
  await processOrder(reference, order);
}

// ✅ Transform object values
const formattedPrices = Object.fromEntries(
  Object.entries(prices).map(([currency, amount]) => [currency, formatCurrency(amount)])
);

// ✅ Object.values — just the values
const allOrders = Object.values(ordersByStatus).flat();
const totalRevenue = Object.values(revenueByRegion)
  .reduce((sum, revenue) => sum + revenue, 0);

// ✅ Build object from entries — useful after filtering
const activeConfig = Object.fromEntries(
  Object.entries(config).filter(([, value]) => value !== null)
);

Array.at() for Indexed Access

Use Array.at() for cleaner access to elements at the start or end:

// ✅ Array.at() — cleaner negative indexing
const lastOrder = orders.at(-1);        // last element
const secondLast = orders.at(-2);       // second to last
const firstOrder = orders.at(0);        // equivalent to orders[0]

// ❌ Verbose equivalent
const lastOrder = orders[orders.length - 1];

// ✅ Useful for finding the most recent item
const latestPayment = payments
  .sort((a, b) => a.createdAt - b.createdAt)
  .at(-1);

structuredClone for Deep Copies

Use structuredClone for deep copying objects and arrays. Never use JSON.parse(JSON.stringify()) for deep cloning:

// ✅ structuredClone — handles dates, arrays, nested objects, circular references
const orderSnapshot = structuredClone(order);
const configCopy = structuredClone(config);

// ❌ JSON round-trip — destroys Dates, undefined, functions, and circular refs
const orderCopy = JSON.parse(JSON.stringify(order));
// order.createdAt is now a string, not a Date object

// ❌ Spread only shallow copies — nested objects are still shared references
const shallowCopy = { ...order };
shallowCopy.items.push(newItem);  // also mutates order.items!

// ✅ For shallow copies where you only need the top level — spread is fine
const updatedOrder = { ...order, status: 'confirmed' };

Classes

// ✅ ES6 class with private fields
class OrderService {
  #repository;
  #paymentService;

  constructor(repository, paymentService) {
    this.#repository = repository;
    this.#paymentService = paymentService;
  }

  async createOrder(request) {
    const order = await this.#buildOrder(request);
    return this.#repository.save(order);
  }

  async #buildOrder(request) { }  // private method
}

// ✅ Static class fields for constants
class ApiClient {
  static #DEFAULT_TIMEOUT = 5000;
  static #BASE_URL = process.env.API_URL;

  static create(options = {}) {
    return new ApiClient({ timeout: ApiClient.#DEFAULT_TIMEOUT, ...options });
  }
}

// ❌ Prototype-based — legacy pattern, do not use
function OrderService(repository) { this.repository = repository; }
OrderService.prototype.createOrder = function(request) { ... };

Modules

Use ES modules (import/export) for all new code:

// ✅ ES module syntax
import { UserService } from './user-service.js';
export class OrderService { }
export const MAX_ORDER_ITEMS = 50;

// ❌ CommonJS — not used in new code
const UserService = require('./user-service');
module.exports = { OrderService };

Error Handling

Typed Error Classes

// ✅ Typed, identifiable errors with context
export class OrderNotFoundError extends Error {
  constructor(orderId) {
    super(`Order not found: ${orderId}`);
    this.name = 'OrderNotFoundError';
    this.orderId = orderId;
  }
}

export class InsufficientStockError extends Error {
  constructor(productId, requested, available) {
    super(`Insufficient stock for product ${productId}`);
    this.name = 'InsufficientStockError';
    this.productId = productId;
    this.requested = requested;
    this.available = available;
  }
}

// Caller distinguishes error types with instanceof
try {
  await orderService.createOrder(request);
} catch (error) {
  if (error instanceof OrderNotFoundError) {
    return res.status(404).json({ error: error.message });
  }
  if (error instanceof InsufficientStockError) {
    return res.status(409).json({ error: error.message });
  }
  throw error; // unknown — rethrow
}

Always Return Promises from Async Operations

// ✅ Awaited — errors are caught
async function sendNotification(order) {
  await emailService.sendConfirmation(order);
}

// ❌ Fire-and-forget — unhandled rejection
function sendNotification(order) {
  emailService.sendConfirmation(order);  // promise not returned or awaited
}

Forbidden Features

Feature Why Forbidden Alternative
var Function-scoped, hoisted, source of bugs const or let
with Pollutes scope, unpredictable Destructuring
eval() Security risk, blocks optimisation No alternative — redesign
Implicit globals Undeclared variable becomes global Always declare with const/let
== equality Type coercion causes silent bugs Always use ===
!= inequality Type coercion Always use !==
arguments object Legacy, breaks with arrow functions Rest parameters ...args
JSON.parse(JSON.stringify()) for deep clone Destroys Dates, undefined, functions structuredClone()
// ❌ Loose equality — type coercion
if (count == 0) { }    // true for '', false, null, undefined too

// ✅ Strict equality
if (count === 0) { }

// ✅ The only accepted loose comparison — intentional null/undefined check
if (userId == null) { }  // catches both null and undefined