Master JavaScript code review patterns including modern syntax, async/await, equality operators, memory leaks, and DOM manipulation. Use PROACTIVELY when reviewing JavaScript PRs.
Install
npx skillscat add tringo0108/z-command/javascript-code-review Install via the SkillsCat registry.
SKILL.md
JavaScript Code Review
Comprehensive code review checklist and patterns for JavaScript applications, focusing on modern syntax, async patterns, and common pitfalls.
When to Use This Skill
- Reviewing JavaScript pull requests
- Establishing JavaScript code review standards
- Training reviewers on JavaScript-specific issues
- Catching common JS bugs and anti-patterns
- Ensuring modern JavaScript best practices
Quick Checklist
## JavaScript Review Checklist
- [ ] Strict equality (===) used, not loose (==)
- [ ] No var, use const/let appropriately
- [ ] Promises properly chained with error handling
- [ ] No callbacks in modern code (use async/await)
- [ ] Array methods used correctly (map/filter/reduce)
- [ ] No prototype pollution risks
- [ ] Event listeners cleaned up
- [ ] Memory leaks checked (closures, listeners)Review Severity Labels
๐ด [blocking] - Must fix before merge (bugs, security, breaking)
๐ก [important] - Should fix, but discuss if you disagree
๐ข [nit] - Nice to have, not blocking
๐ก [suggestion]- Alternative approach to considerModern Syntax
var vs let/const
// โ Using var - hoisting issues, function scoped
var user = "John";
if (true) {
var user = "Jane"; // Overwrites outer variable!
}
console.log(user); // 'Jane'
// โ
Use const by default, let when reassignment needed
const user = "John";
if (true) {
const user = "Jane"; // Block scoped, separate variable
}
console.log(user); // 'John'const vs let
// โ Using let when const would work
let config = { timeout: 5000 };
let users = [];
users.push({ name: "John" }); // Mutating, not reassigning
// โ
Use const for references that don't change
const config = { timeout: 5000 };
const users = [];
users.push({ name: "John" }); // OK - mutating content, not referenceTemplate Literals
// โ String concatenation
const greeting = "Hello, " + name + "! You have " + count + " messages.";
const multiline = "Line 1\n" + "Line 2\n" + "Line 3";
// โ
Template literals
const greeting = `Hello, ${name}! You have ${count} messages.`;
const multiline = `
Line 1
Line 2
Line 3
`;Arrow Functions
// โ Anonymous callbacks
users
.filter(function (user) {
return user.active;
})
.map(function (user) {
return user.name;
});
// โ
Arrow functions
users.filter((user) => user.active).map((user) => user.name);Destructuring
// โ Repetitive property access
function processUser(user) {
console.log(user.name);
console.log(user.email);
console.log(user.role);
}
// โ
Destructuring
function processUser({ name, email, role }) {
console.log(name);
console.log(email);
console.log(role);
}
// โ
With defaults
function processUser({ name, email, role = "user" }) {
// role defaults to 'user' if not provided
}Spread Operator
// โ Object.assign for shallow copy
const newConfig = Object.assign({}, config, { timeout: 10000 });
const allItems = array1.concat(array2);
// โ
Spread operator
const newConfig = { ...config, timeout: 10000 };
const allItems = [...array1, ...array2];Equality and Type Coercion
Strict vs Loose Equality
// โ Loose equality - type coercion bugs
if (value == null) {
} // true for null AND undefined
if (value == 0) {
} // true for 0, '', false, []
if (value == false) {
} // true for 0, '', null, undefined
if ("5" == 5) {
} // true - string coerced!
// โ
Strict equality
if (value === null || value === undefined) {
}
if (value === 0) {
}
if (value === false) {
}
if ("5" === 5) {
} // false - different types
// โ
Exception: null/undefined check (intentional)
if (value == null) {
} // OK for specifically null/undefined checkFalsy vs Nullish
// โ Falsy check when you want null check
function process(value) {
if (!value) {
// Fails for 0, '', false
return "default";
}
return value;
}
process(0); // Returns 'default' - probably wrong!
process(""); // Returns 'default' - probably wrong!
process(false); // Returns 'default' - probably wrong!
// โ
Nullish coalescing for null/undefined only
function process(value) {
return value ?? "default"; // Only null/undefined trigger default
}
process(0); // Returns 0
process(""); // Returns ''
process(false); // Returns false
process(null); // Returns 'default'Truthy Checks with Default Values
// โ Logical OR treats 0, '', false as falsy
const count = options.count || 10; // 0 becomes 10!
const name = options.name || "Guest"; // '' becomes 'Guest'!
// โ
Nullish coalescing
const count = options.count ?? 10; // 0 stays 0
const name = options.name ?? "Guest"; // '' stays ''Async Patterns
Callback Hell
// โ Callback hell (pyramid of doom)
getUser(id, function (user) {
getPosts(user.id, function (posts) {
getComments(posts[0].id, function (comments) {
getAuthor(comments[0].authorId, function (author) {
// deeply nested, hard to maintain
});
});
});
});
// โ
Async/await
async function getPostData(id) {
const user = await getUser(id);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
const author = await getAuthor(comments[0].authorId);
return { user, posts, comments, author };
}Unhandled Promise Rejections
// โ Unhandled promise rejection
async function fetchData() {
const response = await fetch("/api/data");
return response.json();
}
fetchData(); // Rejection silently ignored!
// โ
Always handle errors
async function fetchData() {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Fetch failed:", error);
throw error; // Re-throw or handle appropriately
}
}
fetchData().catch((error) => handleError(error));Sequential vs Parallel
// โ Sequential when parallel is possible
const user = await getUser(id);
const posts = await getPosts(id);
const settings = await getSettings(id);
// Each waits for the previous - slow!
// โ
Parallel execution
const [user, posts, settings] = await Promise.all([
getUser(id),
getPosts(id),
getSettings(id),
]);
// All run simultaneously - fast!Promise.allSettled for Partial Failures
// โ One failure cancels everything
try {
const results = await Promise.all([
fetchUser(1),
fetchUser(2), // If this fails...
fetchUser(3),
]);
} catch (error) {
// All results lost!
}
// โ
Handle partial failures
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(2),
fetchUser(3),
]);
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`User ${index + 1}:`, result.value);
} else {
console.log(`User ${index + 1} failed:`, result.reason);
}
});Memory Leaks
Event Listeners Not Cleaned Up
// โ Event listener not cleaned up - memory leak
class Widget {
constructor() {
window.addEventListener("resize", this.handleResize);
}
handleResize() {
console.log("Resized");
}
// Widget removed but listener stays!
}
// โ
Clean up listeners
class Widget {
constructor() {
this.handleResize = this.handleResize.bind(this);
window.addEventListener("resize", this.handleResize);
}
handleResize() {
console.log("Resized");
}
destroy() {
window.removeEventListener("resize", this.handleResize);
}
}Closures Holding References
// โ Closure holding large data
function createHandler(largeData) {
return function onClick() {
console.log("Clicked");
// largeData is captured in closure but never used!
// Memory never released while handler exists
};
}
// โ
Don't capture unnecessary data
function createHandler() {
return function onClick() {
console.log("Clicked");
};
}
// If data is needed, only capture what's necessary
function createHandler(userName) {
// Just the name, not whole object
return function onClick() {
console.log(`${userName} clicked`);
};
}Timers Not Cleared
// โ Timer continues after component removed
function startPolling() {
setInterval(() => {
fetchData(); // Keeps running forever!
}, 5000);
}
// โ
Clear timer on cleanup
function startPolling() {
const intervalId = setInterval(() => {
fetchData();
}, 5000);
return () => clearInterval(intervalId);
}
// Usage
const stopPolling = startPolling();
// Later...
stopPolling();Subscriptions Not Unsubscribed
// โ Observable not unsubscribed
class Component {
init() {
eventEmitter.on("update", this.handleUpdate);
// Never unsubscribed - memory leak!
}
}
// โ
Unsubscribe on cleanup
class Component {
init() {
this.handleUpdate = this.handleUpdate.bind(this);
eventEmitter.on("update", this.handleUpdate);
}
destroy() {
eventEmitter.off("update", this.handleUpdate);
}
}Array Methods
Incorrect Method Usage
// โ forEach for transformation (should use map)
const names = [];
users.forEach((user) => {
names.push(user.name);
});
// โ
map for transformation
const names = users.map((user) => user.name);
// โ filter + length for counting
const count = items.filter((item) => item.active).length;
// โ
reduce for counting (no intermediate array)
const count = items.reduce((acc, item) => (item.active ? acc + 1 : acc), 0);
// Or just filter if readability matters more than perf
const count = items.filter((item) => item.active).length;Mutating Original Array
// โ sort mutates original array!
const sorted = users.sort((a, b) => a.age - b.age);
// users array is now sorted too!
// โ
Copy before sorting
const sorted = [...users].sort((a, b) => a.age - b.age);
// Or
const sorted = users.toSorted((a, b) => a.age - b.age); // ES2023+
// Same issue with reverse()
const reversed = [...items].reverse();
// Or
const reversed = items.toReversed(); // ES2023+Object Patterns
Prototype Pollution
// โ Dangerous - prototype pollution possible
function merge(target, source) {
for (const key in source) {
target[key] = source[key];
}
return target;
}
// Attacker could set __proto__!
// โ
Check for dangerous keys
function merge(target, source) {
for (const key of Object.keys(source)) {
if (key === "__proto__" || key === "constructor" || key === "prototype") {
continue;
}
target[key] = source[key];
}
return target;
}
// โ
Or use Object.assign / spread
const merged = { ...target, ...source };for...in vs for...of
// โ for...in on arrays - iterates indices as strings, includes prototype
for (const item in array) {
console.log(item); // '0', '1', '2' (strings!)
}
// โ
for...of for arrays - iterates values
for (const item of array) {
console.log(item); // actual values
}
// โ
for...in for objects - iterates keys
for (const key in object) {
if (Object.hasOwn(object, key)) {
// Skip inherited
console.log(key, object[key]);
}
}
// โ
Better: Object.entries
for (const [key, value] of Object.entries(object)) {
console.log(key, value);
}Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
var usage |
Hoisting, function scope | Use const/let |
== equality |
Type coercion bugs | Use === |
| Falsy defaults | 0, '', false treated as null | Use ?? |
| Uncaught promises | Silent failures | Always catch errors |
| No cleanup | Memory leaks | Remove listeners, clear timers |
sort() mutation |
Changes original array | Copy first with [...arr] |
for...in on arrays |
Wrong iteration | Use for...of |
Best Practices Summary
- Use const by default -
letonly when reassignment needed - Strict equality -
===and!==always - Nullish coalescing -
??for defaults, not|| - async/await - Over callbacks and
.then()chains - Always handle errors - No uncaught promise rejections
- Clean up resources - Event listeners, timers, subscriptions
- Don't mutate - Copy before
sort(),reverse(), etc. - Use modern methods -
Object.entries,Array.from, etc.
Parent Hub
Part of Workflow
This skill is utilized in the following sequential workflows: