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() |