
Javascript
- 4 installs
- 19 repo stars
- Updated August 1, 2026
- xobotyi/cc-foundry
Helps with ai & agent building tasks.
About
javascript is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- javascript
- AI & Agent Building
- AI-coding skill
Javascript by the numbers
- 4 all-time installs (skills.sh)
- Ranked #13,348 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xobotyi/cc-foundry --skill javascriptAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 19 |
| Last updated | August 1, 2026 |
| Repository | xobotyi/cc-foundry ↗ |
What it does
Helps with ai & agent building tasks.
Files
JavaScript
Clarity is the highest JavaScript virtue. If your code requires a comment to explain its control flow, rewrite it.
JavaScript rewards explicit, readable code. Prefer boring patterns that are easy to understand over clever tricks that save characters.
References
- Functions, closures, composition → [
${CLAUDE_SKILL_DIR}/references/functions.md] — Arrow function examples,
closure patterns, early return, parameter destructuring
- Async patterns, error handling, concurrency → [
${CLAUDE_SKILL_DIR}/references/async.md] — Promise.all/race/any
examples, cancellation, custom error classes, for-await
- Objects, arrays, iteration, Map/Set → [
${CLAUDE_SKILL_DIR}/references/objects-and-arrays.md] — Iteration
decision table, destructuring patterns, immutable updates, generators
- ES modules, imports, barrel files → [
${CLAUDE_SKILL_DIR}/references/modules.md] — Import ordering, barrel file
rationale, directory import pitfalls, dynamic imports
- JSDoc typing, full tag catalog → [
${CLAUDE_SKILL_DIR}/references/jsdoc.md] — Full tag reference (@callback,
@template, @enum), type assertions, class modifiers
- General JS idioms and edge cases → [
${CLAUDE_SKILL_DIR}/references/idioms.md] — Variable/naming examples,
equality coercion table, modern syntax patterns
Variables and Declarations
- `const` by default. Use
letonly when reassignment is required. Nevervar. - `const` prevents reassignment, not mutation. Objects and arrays declared with
constcan still be mutated. - Block scope only.
let/constare block-scoped;varis function-scoped and hoists — this causes bugs in loops
and conditionals.
- One declaration per line. Never chain
const a = 1, b = 2. - Group declarations.
constfirst, thenlet.
Naming
| Entity | Style | Examples |
|---|---|---|
| Variables, functions | camelCase | userName, fetchData |
| Classes, constructors | PascalCase | UserService, HttpClient |
| True compile-time constants | SCREAMING_SNAKE_CASE | MAX_RETRIES, API_BASE_URL |
| Private fields/methods | # prefix (class) | #count, #validate() |
| Booleans | is/has/can/should prefix | isValid, hasAccess |
| File names | kebab-case or camelCase | user-service.js, userService.js |
- SCREAMING_SNAKE_CASE is for true constants only — values known at compile time, never computed at runtime. A
variable holding a function return value uses camelCase.
- Descriptive names.
userCountnotn. Short names (i,x) only in tiny scopes (loop indices, simple arrow
callbacks).
- Accepted abbreviations:
url,id,err,ctx,req,res— universally understood. Avoid all others. - No redundant context.
car.makenotcar.carMake. - Consistent vocabulary. Use the same word for the same concept throughout a codebase —
getUser()everywhere, not
getUserInfo() / getClientData() / getCustomerRecord().
Equality and Safety
- Always `===` and `!==`. Never
==except forvalue == null(checks bothnullandundefined). - `??` over `||` for defaults —
||treats0,"",falseas falsy. - `?.` for optional access. Don't overuse — missing data you expect should throw, not silently return
undefined. - Know the falsy values:
false,0,-0,0n,"",null,undefined,NaN. Everything else is truthy,
including [], {}, and "0".
Ternary Operator
- One-liners or split-per-branch only. Ternaries are acceptable in two forms:
// OK — fits on one line
const label = isActive ? 'Active' : 'Inactive';
// OK — each branch on its own line
const label = isActive
? buildActiveLabel(user)
: buildInactiveLabel(user);Any ternary that doesn't fit one of these two patterns must be rewritten as if/else or early return.
- Nested ternaries are banned. No exceptions. Use
if/else, early returns, or a lookup object.
Modern Syntax
- Template literals for string interpolation: `
Hello, ${name}`. Don't use template literals for strings
without interpolation — use plain quotes.
- Spread for copies:
{ ...obj }and[...arr]. NeverObject.assign. - Rest parameters to collect remaining:
const { id, ...rest } = user. - Shorthand properties:
{ name, age }not{ name: name, age: age }. Group shorthand properties at the top of
object literals.
- Computed property names:
{ [key]: value, [${key}Date]: new Date() }. - Logical assignment operators:
opts.timeout ??= 5000(assign if nullish),opts.name ||= "default"(assign if
falsy), opts.handler &&= wrap(opts.handler) (assign if truthy).
Functions
- Arrow functions for callbacks and anonymous functions. Use function declarations only when hoisting or
this
binding is needed.
- Prefer parentheses around arrow function parameters even for single params — smaller diffs when adding/removing
parameters.
- Implicit return for single expressions (no braces). Explicit return (braces) for multi-statement bodies.
- Arrow functions capture lexical `this` — they do NOT have their own
this. Never use arrow functions as object
methods or on prototypes.
- Destructured options for 3+ parameters. Self-documenting and order-independent.
- Default parameters over
||or manual checks. Defaults are evaluated left-to-right and can reference earlier
params.
- Rest parameters over
argumentsobject.argumentsis array-like, not a real Array. - Early return. Guard clauses first, happy path flat. Reduce nesting.
- One function, one job. If the name contains "and", split it.
- Keep functions under ~30 lines. Extract helpers. Use composition over complex branching.
- Prefer pure functions (same input = same output, no side effects). Isolate side effects (DOM, network, logging) —
don't hide them inside data transformations.
- Closures retain references to outer variables, not copies. Be cautious with large objects captured unintentionally
— they won't be garbage collected until the closure is released.
Async
- `async`/`await` over `.then()` chains for sequential operations.
- Always `await` promises. Missing
await= floating promise = silent failures. - `return await` only inside `try` blocks where you need to catch the awaited error. Otherwise just
return promise
— no need for async wrapper.
- `Promise.all` for independent parallel work. Rejects on first rejection.
- `Promise.allSettled` when all results matter regardless of individual failures.
- `Promise.race` for timeouts. `Promise.any` for fallbacks (rejects only when ALL reject).
- Avoid sequential awaits in loops. Use
Promise.all(items.map(...))for parallel. Use a concurrency limiter (e.g.
p-map) for large arrays.
- Throw `Error` objects, never strings or plain objects — strings lose stack traces.
- Custom error classes when callers need to distinguish errors: extend
Error, setthis.name, add context
properties.
- Never swallow errors. Every
catchmust handle, rethrow, or report. Emptycatchblocks hide bugs.
console.log(err) alone is not handling.
- Let errors propagate to a top-level handler when possible. Don't wrap every
awaitintry/catch— only where
you need to handle at that level.
- Attach `.catch()` to non-awaited promise chains. Unhandled rejections crash Node.js. Fire-and-forget:
fetchData().catch(reportError).
- Only use `new Promise()` to wrap callback-based APIs. Most async code should compose existing promises with
async/await.
- `AbortController` for cancellable async operations: pass
{ signal }tofetchand other APIs. - `for await...of` for async iterables (streams, async generators).
Modules
- ES modules only.
import/exportfor all new code. CommonJS (require) is legacy — use only when runtime
requires it.
- Named exports over default exports. Default exports cause inconsistent naming across importers. Exception: default
exports acceptable when required by framework convention (Next.js pages, Remix routes).
- Don't export mutable `let` bindings. Export accessor functions instead:
export function getCount()not
export let count.
- Imports at the top, grouped with blank lines: built-in (
node:fs), external (express), internal (./utils). - Always include file extensions in import paths —
"./user.js", not"./user". Extensionless imports vary across
runtimes.
- No directory imports. Import from the file directly, not from a folder that resolves to
index.js. - No barrel files in subdirectories.
index.jsre-exports create indirection and hurt tree-shaking. Acceptable only
as a standalone package entry point where the runtime can enforce the boundary via package.json exports.
- No circular dependencies. Extract shared code to a third module, merge tightly coupled modules, or use dependency
injection.
- No wildcard re-exports. Explicit re-exports only — wildcards bypass tree-shaking.
- Merge imports from the same module into a single statement.
- Namespace imports (
import * as dateFns) for large modules (5+ items). Prefer named imports when importing fewer
than ~5 items.
- Dynamic imports (
import()) for code splitting and lazy loading: routes loaded on navigation, large conditional
dependencies, feature flags.
- One concern per module. If a module exports unrelated functionality, split it.
- Side-effect imports (
import "./polyfill.js") should be rare. Document why.
Objects and Arrays
- Literal syntax.
{}and[], nevernew Object()/new Array(). - Use method shorthand on objects:
greet() { }notgreet: function() { }. - Spread for copies.
{ ...obj }and[...arr]. Prefer overObject.assign. - Destructure to extract properties. Prefer parameter destructuring.
- Dot notation for static properties, brackets for dynamic:
user.namevsuser[dynamicKey]. - `Object.hasOwn(obj, key)` instead of
obj.hasOwnProperty(key). - Functional array methods (
map,filter,find,some,every,flatMap,reduce) over imperative loops for
data transformation.
- Always return in
map,filter,reducecallbacks. - `Array.from(arrayLike)` for array-like objects (not spread).
Array.from(iterable, mapFn)instead of
[...iterable].map(mapFn) — avoids intermediate array.
- Don't mutate inputs. Return new objects/arrays. Immutable update patterns: add
[...arr, item], remove
arr.filter(...), update arr.map(...).
- `for...of` for side-effect loops. Never
for...inon arrays. - Return objects for multiple values, not arrays — callers don't depend on order.
- Never extend built-in prototypes (
Array.prototype,Object.prototype). Use utility functions or subclasses.
Prefer for...of for side-effect loops, Array.prototype methods (.map, .filter, .reduce, .find, .some, .every) for data transforms, for for index-needed loops. See ${CLAUDE_SKILL_DIR}/references/objects-and-arrays.md for the full iteration decision table.
Use Map when keys aren't strings or are user-provided (avoids prototype pollution). Use Set for dedup ([...new Set(items)]). Use generators (function*) for lazy sequences and deferred computation.
Classes
- ES `class` syntax only. No function constructors or prototype manipulation.
- `#private` fields for encapsulation. Not
_convention. - Composition over inheritance. Use
extendsonly for true "is-a" relationships. - No empty constructors. If the constructor only calls
super(), omit it. - Static methods for operations that don't need instance state.
- Methods can return `this` for fluent/chainable APIs.
- Don't force classes when plain functions and objects suffice. A class with one method is a function in disguise.
JSDoc Typing
For pure JavaScript projects that don't use TypeScript, use JSDoc annotations to provide type safety through editor tooling. Enable // @ts-check at file top or checkJs in jsconfig.json.
Core tags: @type, @param, @returns, @typedef (with @property), @template. See ${CLAUDE_SKILL_DIR}/references/jsdoc.md for the full tag catalog including @callback, @enum, class modifiers, and type import syntax.
JSDoc Best Practices
- Annotate public API boundaries — exported functions, classes, module-level variables. Internal code often needs
fewer annotations; types flow from context.
- Prefer inline TypeScript syntax in JSDoc types:
{string | number}over{(string|number)}. - Use `@typedef` for shared shapes — define once near file top or in
types.js. - Don't annotate the obvious — if
const x = 5is clearly a number, skip@type.
Doc Comments
Doc comments (/** */) are API documentation, not code comments — the "no comments" default does not apply.
Every exported function, class, and module-level constant gets a doc comment. Include @param, @returns, @throws for non-trivial signatures. Describe behavior and intent, not implementation.
When modifying an exported symbol's behavior or signature, update its doc comment in the same edit.
Application
When writing JavaScript code:
- Apply all conventions silently — don't narrate each rule being followed.
- If an existing codebase contradicts a convention, follow the codebase and flag the divergence once.
When reviewing JavaScript code:
- Cite the specific violation and show the fix inline.
- Don't lecture or quote the rule — state what's wrong and how to fix it.
Bad review comment:
"According to best practices, you should use const instead of let
when the variable is never reassigned."
Good review comment:
"`let` -> `const` — `config` is never reassigned."Code Navigation — LSP Required
A typescript-language-server LSP server is configured for all JS/TS file types (.js, .jsx, .ts, .tsx, .mjs, .cjs, .mts, .cts). Always use LSP tools for code navigation instead of Grep or Glob. LSP understands module resolution, type inference, scope rules, and project boundaries — text search does not.
Tool Routing
- Find where a function/class/variable is defined →
goToDefinition— Resolves imports, re-exports, aliases - Find all usages of a symbol →
findReferences— Scope-aware, no false positives from string matches - Get type signature, docs, or return types →
hover— Instant type info without reading source files - List all exports/symbols in a file →
documentSymbol— Structured output vs grepping for
function/class/export
- Find a symbol by name across the project →
workspaceSymbol— Searches all modules - Find implementations of an interface →
goToImplementation— Knows the type system - Find what calls a function →
incomingCalls— Precise call graph across module boundaries - Find what a function calls →
outgoingCalls— Structured dependency map
Grep/Glob remain appropriate for: text in comments, string literals, log messages, TODO markers, config values, env vars, CSS classes, file name patterns, URLs, error message text.
When spawning subagents for JS/TS codebase exploration, instruct them to use LSP tools. Subagents have access to the same LSP server.
Integration
The coding skill governs workflow; this skill governs JavaScript implementation choices. For TypeScript projects, the typescript skill extends this one.
{
"sources": {
"Google JavaScript Style Guide": "https://google.github.io/styleguide/jsguide.html",
"Airbnb JavaScript Style Guide": "https://raw.githubusercontent.com/airbnb/javascript/master/README.md",
"Clean Code JavaScript": "https://raw.githubusercontent.com/ryanmcdermott/clean-code-javascript/master/README.md",
"MDN - JavaScript Modules": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/javascript/guide/modules/index.md",
"MDN - Using Promises": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/javascript/guide/using_promises/index.md",
"MDN - Iterators and Generators": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/javascript/guide/iterators_and_generators/index.md",
"MDN - Closures": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/javascript/guide/closures/index.md",
"MDN - Control Flow and Error Handling": "https://raw.githubusercontent.com/mdn/content/main/files/en-us/web/javascript/guide/control_flow_and_error_handling/index.md",
"TypeScript JSDoc Reference": "https://raw.githubusercontent.com/microsoft/TypeScript-Website/v2/packages/documentation/copy/en/javascript/JSDoc%20Reference.md"
},
"lastFetched": "2026-02-16T13:19:39.193Z"
}
JavaScript Async Patterns
Promises, async/await, error handling, and concurrency utilities.
async/await First
Use async/await as the default for asynchronous code. It reads top-to-bottom like synchronous code and makes error handling straightforward.
// Good — clear sequential flow
async function fetchUserPosts(userId) {
const user = await getUser(userId);
const posts = await getPosts(user.id);
return posts;
}
// Avoid — .then() chains for sequential operations
function fetchUserPosts(userId) {
return getUser(userId)
.then((user) => getPosts(user.id));
}Key Rules
- Always `await` promises. A missing
awaitcreates a floating promise — the operation runs but its result and
errors are silently lost.
- Mark the function `async` if it uses
await. - Return values, not `return await`. In a non-try/catch context,
return promiseandreturn await promisebehave
identically. Use return await only inside try blocks where you need to catch the awaited error.
// Unnecessary await
async function getUser(id) {
return await fetchUser(id); // just: return fetchUser(id);
}
// Necessary await — catch needs it
async function getUser(id) {
try {
return await fetchUser(id);
} catch (err) {
return null;
}
}Error Handling
try/catch with async/await
Wrap await calls in try/catch when you need to handle errors at that level. Don't wrap everything — let errors propagate to a top-level handler when possible.
// Good — granular error handling where needed
async function loadConfig() {
try {
const data = await readFile("config.json", "utf8");
return JSON.parse(data);
} catch (err) {
if (err.code === "ENOENT") return DEFAULT_CONFIG;
throw err; // re-throw unexpected errors
}
}Never Swallow Errors
Every catch must do something meaningful: rethrow, return a fallback, or report. An empty catch hides bugs.
// Bad — error silently disappears
try { await riskyOperation(); } catch (err) {}
// Bad — console.log is not handling
try { await riskyOperation(); } catch (err) { console.log(err); }
// Good — handle or propagate
try {
await riskyOperation();
} catch (err) {
reportError(err);
throw err;
}Throw Error Objects, Not Strings
Always throw Error instances (or subclasses). String throws lose stack traces:
// Bad — no stack trace
throw "Something went wrong";
throw { message: "fail" };
// Good
throw new Error("Something went wrong");
throw new TypeError(`Expected string, got ${typeof value}`);Custom Error Classes
For errors callers need to distinguish, use custom error classes:
class NotFoundError extends Error {
constructor(resource, id) {
super(`${resource} ${id} not found`);
this.name = "NotFoundError";
this.resource = resource;
this.id = id;
}
}
// Usage
throw new NotFoundError("User", userId);
// Catching
try { ... } catch (err) {
if (err instanceof NotFoundError) {
return res.status(404).json({ error: err.message });
}
throw err;
}Unhandled Rejections
Always attach .catch() to promise chains that aren't awaited. Unhandled rejections crash Node.js and produce console errors in browsers:
// Bad — floating promise, errors lost
fetchData();
// Good — fire-and-forget with error handling
fetchData().catch(reportError);
// Good — top-level await (ESM)
await fetchData();Concurrency
Promise.all — Parallel Independent Work
When operations are independent, run them in parallel:
// Bad — sequential when it doesn't need to be
const users = await getUsers();
const posts = await getPosts();
const comments = await getComments();
// Good — parallel
const [users, posts, comments] = await Promise.all([
getUsers(),
getPosts(),
getComments(),
]);Promise.all rejects as soon as any promise rejects. The other promises continue running but their results are not available.
Promise.allSettled — When All Results Matter
Use when you need results from all operations regardless of individual failures:
const results = await Promise.allSettled([
fetchFromPrimary(),
fetchFromFallback(),
]);
const successes = results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);Promise.race and Promise.any
- `Promise.race`: resolves/rejects with the first settled promise. Use for timeouts.
- `Promise.any`: resolves with the first fulfilled promise. Rejects only when ALL promises reject. Use for
fallbacks.
// Timeout pattern
const result = await Promise.race([
fetchData(),
timeout(5000),
]);
// Fallback pattern
const data = await Promise.any([
fetchFromCDN(),
fetchFromOrigin(),
]);Avoid Sequential Awaits in Loops
// Bad — each iteration waits for the previous one
for (const url of urls) {
const data = await fetch(url); // sequential!
}
// Good — parallel when order doesn't matter
const results = await Promise.all(urls.map((url) => fetch(url)));
// Good — controlled concurrency for large arrays
// (use a library like p-map for concurrency limiting)Promise Construction
Avoid the Constructor When Unnecessary
Most async code should compose existing promises with async/await. Only use new Promise() to wrap callback-based APIs:
// Unnecessary — already have a promise
const result = new Promise((resolve) => {
resolve(existingPromise); // just return existingPromise directly
});
// Legitimate — wrapping a callback API
function readFileAsync(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, "utf8", (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}Cancellation with AbortController
Use AbortController for cancellable async operations:
const controller = new AbortController();
const { signal } = controller;
const response = await fetch(url, { signal });
// Cancel from elsewhere
controller.abort();for-await-of
Use for await...of with async iterables:
async function processStream(stream) {
for await (const chunk of stream) {
process(chunk);
}
}JavaScript Functions
Function declarations, arrow functions, closures, scope, and parameter patterns.
Function Style
Arrow Functions by Default
Use arrow functions for anonymous functions and callbacks. Use function declarations only when hoisting or this binding is needed.
// Callbacks — always arrow
const doubled = items.map((x) => x * 2);
setTimeout(() => cleanup(), 1000);
// Named functions — declaration or const, pick one per codebase
function processOrder(order) { ... }
// or
const processOrder = (order) => { ... };Arrow Function Syntax Rules
// Single param — parens optional but prefer them for consistency
const square = (x) => x * x;
// No params — parens required
const greet = () => "hello";
// Single expression — implicit return (no braces)
const add = (a, b) => a + b;
// Multi-statement — explicit return (braces required)
const transform = (data) => {
const normalized = normalize(data);
return validate(normalized);
};Prefer parentheses around arrow function parameters even when there is only one — it makes adding/removing parameters a smaller diff.
Arrow Functions and this
Arrow functions capture this from the enclosing lexical scope. They do NOT have their own this.
// Good — arrow preserves `this` from class method
class Timer {
start() {
this.id = setInterval(() => this.tick(), 1000);
}
}
// Bad — regular function has its own `this` (undefined in strict mode)
class Timer {
start() {
this.id = setInterval(function () {
this.tick(); // TypeError: this.tick is not a function
}, 1000);
}
}Never use arrow functions as methods on objects or prototypes — they won't have the correct this:
// Bad — arrow captures module-level this, not the object
const obj = {
name: "test",
greet: () => `Hello, ${this.name}`, // this.name is undefined
};
// Good
const obj = {
name: "test",
greet() { return `Hello, ${this.name}`; },
};Parameters
Default Parameters
Use default parameter syntax. Never mutate arguments or use || for defaults:
// Bad — || fails on falsy values like 0, ""
function create(name, timeout) {
timeout = timeout || 5000;
}
// Good
function create(name, timeout = 5000) { ... }Default parameters are evaluated left to right and can reference earlier params:
function createElement(tag, className = `${tag}-default`) { ... }Destructured Options
For functions with 3+ parameters, use a destructured options object:
// Bad — positional args are hard to remember
function createUser(name, email, role, active) { ... }
createUser("Alice", "a@b.com", "admin", true);
// Good — self-documenting, order-independent
function createUser({ name, email, role = "user", active = true }) { ... }
createUser({ name: "Alice", email: "a@b.com", role: "admin" });Rest Parameters Over arguments
Never use the arguments object. Use rest parameters instead:
// Bad — arguments is array-like, not a real Array
function sum() {
return Array.prototype.slice.call(arguments).reduce((a, b) => a + b, 0);
}
// Good
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}Closures
A closure is a function that captures variables from its enclosing scope. Every function in JavaScript forms a closure.
Factory Pattern
function createCounter(initial = 0) {
let count = initial;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count,
};
}Loop Closure Pitfall
The classic var-in-loop bug is caused by function-scoped var. Use let or const in loops to avoid it:
// Bug — var is shared across all closures
for (var i = 0; i < 5; i++) {
buttons[i].onclick = () => console.log(i); // always 5
}
// Fix — let creates block scope per iteration
for (let i = 0; i < 5; i++) {
buttons[i].onclick = () => console.log(i); // 0, 1, 2, 3, 4
}Memory Consideration
Closures retain references to outer variables, not copies. Be cautious with large objects captured unintentionally — they won't be garbage collected until the closure is released.
Pure Functions
Prefer pure functions (same input = same output, no side effects):
// Impure — mutates input
const addItem = (cart, item) => {
cart.push(item);
return cart;
};
// Pure — returns new array
const addItem = (cart, item) => [...cart, item];Side effects (DOM manipulation, network calls, logging) should be isolated and explicit, not hidden inside data transformation functions.
Function Size and Composition
Functions should do one thing. If a function name contains "and", it should probably be two functions.
// Bad — does two things
function validateAndSave(user) { ... }
// Good — separate concerns
function validate(user) { ... }
function save(user) { ... }Keep functions short. If a function exceeds ~30 lines, consider extracting helpers. Use composition over complex branching:
const process = pipe(normalize, validate, transform, persist);Early Return
Return early to reduce nesting and keep the happy path flat:
// Bad — deep nesting
function getUser(id) {
if (id) {
const user = db.find(id);
if (user) {
if (user.active) {
return user;
}
}
}
return null;
}
// Good — flat and readable
function getUser(id) {
if (!id) return null;
const user = db.find(id);
if (!user) return null;
if (!user.active) return null;
return user;
}JavaScript Idioms
Variables, naming, declarations, equality, type coercion, and modern syntax patterns.
Variables
const by Default
Use const for all bindings. Switch to let only when reassignment is required. Never use var.
// Good
const maxRetries = 3;
let attempts = 0;
// Bad — var is function-scoped, not block-scoped
var count = 0;const does not make values immutable — it prevents reassignment of the binding. Objects and arrays declared with const can still be mutated.
Block Scoping
let and const are block-scoped. var is function-scoped and hoisted to the function top, which causes bugs in loops and conditionals:
// Bug — var is shared across all iterations
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // prints 3, 3, 3
}
// Fix — let creates a new binding per iteration
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // prints 0, 1, 2
}One Declaration Per Line
Declare each variable on its own line. Never chain declarations.
// Bad
const a = 1, b = 2, c = 3;
// Good
const a = 1;
const b = 2;
const c = 3;Group const declarations first, then let declarations.
Naming
Conventions
| Entity | Style | Examples |
|---|---|---|
| Variables, functions | camelCase | userName, fetchData |
| Classes, constructors | PascalCase | UserService, HttpClient |
| Constants (true constants) | SCREAMING_SNAKE_CASE | MAX_RETRIES, API_BASE_URL |
| Private fields/methods | # prefix (class) | #count, #validate() |
| Boolean variables | is/has/can/should prefix | isValid, hasAccess |
| File names | kebab-case or camelCase | user-service.js, userService.js |
True constants are values known at compile time and never computed at runtime. A variable that holds a value returned from a function is not a constant — use camelCase for those:
// SCREAMING_SNAKE — known at compile time
const MAX_RETRIES = 3;
const API_BASE_URL = "https://api.example.com";
// camelCase — computed or runtime-dependent
const currentUser = await getUser();
const defaultTimeout = config.timeout ?? 5000;Descriptive Names
Names should reveal intent. Avoid abbreviations unless universally understood (url, id, err, ctx, req, res).
// Bad
const d = new Date();
const cb = (x) => x * 2;
// Good
const now = new Date();
const double = (value) => value * 2;Short names are acceptable in short scopes: loop indices (i, j), arrow function params in simple callbacks (x => x.id).
Avoid Redundant Context
Don't repeat the containing object/class name in property names:
// Bad
const car = { carMake: "Honda", carModel: "Accord" };
// Good
const car = { make: "Honda", model: "Accord" };Consistent Vocabulary
Use the same word for the same concept throughout a codebase:
// Bad — three names for the same concept
getUserInfo();
getClientData();
getCustomerRecord();
// Good
getUser();Equality and Comparisons
Always Use === and !==
Abstract equality (==) performs type coercion and produces surprising results:
0 == "" // true
0 == "0" // true
"" == "0" // false
false == "0" // true
null == undefined // trueUse strict equality (===) everywhere. The only acceptable use of == is checking for null or undefined together:
// Acceptable — checks both null and undefined
if (value == null) { ... }
// Equivalent explicit version
if (value === null || value === undefined) { ... }Falsy Values
These are falsy in JavaScript: false, 0, -0, 0n, "", null, undefined, NaN.
Everything else is truthy, including [], {}, and "0".
Nullish Coalescing Over ||
Use ?? when you want to default only on null/undefined. Use || only when you want to default on all falsy values:
// Bug — || treats 0 and "" as falsy
const port = config.port || 3000; // 0 becomes 3000
// Fix — ?? only defaults on null/undefined
const port = config.port ?? 3000; // 0 stays 0Optional Chaining
Use ?. for safe property access on potentially nullish values:
const city = user?.address?.city;
const result = callback?.();
const item = arr?.[index];Don't overuse — if a value should always exist, accessing it directly is better because it surfaces bugs rather than hiding them.
Modern Syntax
Template Literals
Use template literals for string interpolation and multiline strings:
// Bad
const msg = "Hello, " + name + "! You have " + count + " items.";
// Good
const msg = `Hello, ${name}! You have ${count} items.`;Don't use template literals for strings without interpolation — use plain quotes.
Spread and Rest
// Spread — shallow copy
const copy = { ...original, newProp: true };
const merged = [...arr1, ...arr2];
// Rest — collect remaining
const { id, ...rest } = user;
function log(message, ...args) { ... }Prefer spread { ...obj } over Object.assign({}, obj).
Shorthand Properties
const name = "Alice";
const age = 30;
// Bad
const user = { name: name, age: age };
// Good
const user = { name, age };Group shorthand properties at the top of object literals for readability.
Computed Property Names
const key = "status";
const obj = {
[key]: "active",
[`${key}Date`]: new Date(),
};Logical Assignment
// Assign only if nullish
options.timeout ??= 5000;
// Assign only if falsy
options.name ||= "default";
// Assign only if truthy
options.handler &&= wrapHandler(options.handler);JSDoc for JavaScript
Type annotations for pure JavaScript projects using JSDoc comments. These annotations are understood by TypeScript's checkJs mode, VS Code IntelliSense, and other tooling without requiring a TypeScript compilation step.
For TypeScript projects, use native TS syntax instead — this reference is for .js files only.
Core Tags
@type — Annotate Variables
/** @type {string} */
let name;
/** @type {number[]} */
const scores = [];
/** @type {Map<string, User>} */
const cache = new Map();
/** @type {string | null} */
let result = null;Use full TypeScript type syntax inside JSDoc types — unions, generics, tuples, and utility types all work.
@param and @returns — Annotate Functions
/**
* Fetches a user by ID.
* @param {string} id - The user's unique identifier.
* @param {{ cache?: boolean }} [options] - Fetch options.
* @returns {Promise<User>} The resolved user.
*/
async function getUser(id, options) { ... }Optional parameters use square brackets: @param {string} [name] or @param {string} [name="default"].
Nested object properties use dot notation:
/**
* @param {Object} config
* @param {string} config.host
* @param {number} config.port
* @param {boolean} [config.ssl=false]
*/
function connect(config) { ... }@typedef — Define Reusable Types
/**
* @typedef {Object} User
* @property {string} id
* @property {string} name
* @property {string} email
* @property {boolean} [active]
*/
/** @type {User} */
const user = { id: "1", name: "Alice", email: "a@b.com" };Inline form for simpler types:
/** @typedef {{ id: string, name: string }} UserSummary */@callback — Define Function Types
/**
* @callback Predicate
* @param {string} value
* @param {number} index
* @returns {boolean}
*/
/** @type {Predicate} */
const isLong = (value) => value.length > 10;Generics with @template
/**
* @template T
* @param {T[]} items
* @param {(item: T) => boolean} predicate
* @returns {T | undefined}
*/
function find(items, predicate) {
for (const item of items) {
if (predicate(item)) return item;
}
}
/**
* @template {string} K
* @template V
* @param {K} key
* @param {V} value
* @returns {Record<K, V>}
*/
function createEntry(key, value) {
return /** @type {Record<K, V>} */ ({ [key]: value });
}Constrained generics: @template {string} K means K must extend string.
Type Assertions (Casts)
const el = /** @type {HTMLInputElement} */ (document.getElementById("email"));
el.value = "test";
// const assertion
const config = /** @type {const} */ ({ retries: 3 });Always wrap the expression in parentheses after the @type cast.
Importing Types
Use @import to bring types from other files (TypeScript 5.5+):
/** @import { User } from "./types.js" */
/** @type {User} */
const user = getUser();Inline import (works in all TypeScript versions):
/** @type {import("./types.js").User} */
const user = getUser();Classes
class UserService {
/** @type {Map<string, User>} */
#cache = new Map();
/**
* @param {import("./db.js").Database} db
*/
constructor(db) {
/** @private */
this.db = db;
}
/**
* @param {string} id
* @returns {Promise<User>}
*/
async getUser(id) { ... }
}Property Modifiers
@private— only accessible within the class@protected— accessible in class and subclasses@readonly— set only during initialization@override— marks method as overriding a base class method
Extends with Generics
/**
* @template T
* @extends {Set<T>}
*/
class UniqueList extends Set {
/** @param {T[]} items */
constructor(items) {
super(items);
}
}Enums
/** @enum {number} */
const Status = {
Active: 0,
Inactive: 1,
Suspended: 2,
};JSDoc enums are object literals with typed values — simpler than TypeScript enums but sufficient for many use cases.
Documentation Tags
These work in both .js and .ts files:
/** @deprecated Use newFunction() instead. */
function oldFunction() { ... }
/** @see UserService for the full implementation. */
/** Returns a {@link User} object. */Best Practices
- Annotate public API boundaries — exported functions, classes, and module-level variables. Internal/private code
often needs fewer annotations because types flow from context.
- Prefer inline TypeScript syntax in JSDoc types:
{string | number}over{(string|number)}. - Use `@typedef` for shared shapes — define once near the top of the file or in a dedicated
types.jsfile, then
reference with @type.
- Enable `// @ts-check` at the top of files (or
checkJsinjsconfig.json) to get type errors in your editor. - Don't annotate the obvious — if
const x = 5is clearly a number, skip the@type. Annotate when types are
ambiguous or at API boundaries.
JavaScript Modules
ES module syntax, import/export patterns, and file organization.
ES Modules Only
Use ES modules (import/export) for all new code. CommonJS (require/ module.exports) is legacy — use it only when the runtime or tooling requires it.
Exports
Named Exports by Default
Use named exports. They provide consistent naming across the codebase and enable tree-shaking:
// Good — named exports
export function createUser(data) { ... }
export const MAX_RETRIES = 3;
export class UserService { ... }
// Also good — export list at bottom
function createUser(data) { ... }
const MAX_RETRIES = 3;
export { createUser, MAX_RETRIES };Avoid Default Exports
Default exports cause inconsistent naming — each importing module can use any name, making refactoring harder:
// Bad — default export
export default class UserService { ... }
// Importers pick arbitrary names:
import UserService from "./user-service.js";
import Users from "./user-service.js"; // same thing, different name
import Svc from "./user-service.js"; // even worse
// Good — named export
export class UserService { ... }
// Importers always use the same name:
import { UserService } from "./user-service.js";Exception: default exports are acceptable when required by a framework convention (e.g., Next.js pages, Remix routes).
Don't Export Mutables
Don't export let bindings that get mutated. Export accessor functions instead:
// Bad — mutation visible across modules, confusing
export let count = 0;
export function increment() { count++; }
// Good — controlled access
let count = 0;
export function getCount() { return count; }
export function increment() { count++; }Imports
Always Include File Extensions
Always include file extensions in import paths. Extensionless imports rely on resolution algorithms that vary across runtimes and bundlers — explicit extensions are unambiguous and work everywhere:
// Bad — extensionless
import { db } from "./database";
import { validate } from "../utils/validation";
// Good — explicit extension
import { db } from "./database.js";
import { validate } from "../utils/validation.js";This applies to all relative imports. External package imports ("express", "zod") use the package name as-is.
No Directory Imports
Don't import from a directory path. Directory imports resolve to index.js, which creates implicit coupling to barrel files and hides the actual source:
// Bad — directory import, resolves to ./services/index.js
import { UserService } from "./services";
// Good — import from the actual file
import { UserService } from "./services/user.js";Import at the Top
All imports must be at the top of the file, before any other code:
// Good
import { readFile } from "node:fs/promises";
import { UserService } from "./services/user.js";
const config = loadConfig();Import Ordering
Group imports in this order, separated by blank lines:
1. Built-in/Node modules — node:fs, node:path 2. External packages — express, zod 3. Internal/project imports — ./utils, ../services
import { readFile } from "node:fs/promises";
import express from "express";
import { z } from "zod";
import { db } from "./database.js";
import { validate } from "../utils/validation.js";Don't Import the Same File Multiple Times
Merge imports from the same module into a single statement:
// Bad
import { createUser } from "./user.js";
import { deleteUser } from "./user.js";
// Good
import { createUser, deleteUser } from "./user.js";Namespace Imports for Large Modules
When importing many items from a module, use namespace imports to avoid long destructuring:
// Cluttered
import { parse, format, add, sub, isValid, isBefore } from "date-fns";
// Cleaner
import * as dateFns from "date-fns";
dateFns.parse(...);Use this sparingly — named imports are preferred when you import fewer than ~5 items.
Avoid Wildcard Re-exports
Don't re-export everything from a module — it bypasses tree-shaking and creates opaque APIs:
// Bad — barrel file re-exporting everything
export * from "./user.js";
export * from "./post.js";
export * from "./comment.js";
// Good — explicit re-exports
export { createUser, getUser } from "./user.js";
export { createPost } from "./post.js";Dynamic Imports
Use import() for code splitting and lazy loading:
// Load on demand
const { heavy } = await import("./heavy-module.js");
// Conditional loading
if (needsPolyfill) {
await import("./polyfill.js");
}Dynamic imports return a promise that resolves to the module namespace. Use them for:
- Routes/pages loaded on navigation
- Large dependencies used conditionally
- Feature flags
Module Structure
One Concern Per Module
Each module should have a clear, single purpose. If a module exports unrelated functionality, split it.
No Barrel Files in Subdirectories
Don't create index.js files that re-export from sibling modules just to aggregate a directory's exports. Barrel files add indirection, hurt tree-shaking, create circular dependency risks, and hide the real source of imports:
// Bad — services/index.js barrel re-exporting siblings
export { UserService } from "./user.js";
export { PostService } from "./post.js";
// Bad — consuming code imports from the barrel
import { UserService } from "./services"; // directory import
import { UserService } from "./services/index.js"; // explicit but still a barrel
// Good — import directly from the source file
import { UserService } from "./services/user.js";
import { PostService } from "./services/post.js";Exception: standalone package entry points. A top-level index.js that defines a package's public API is acceptable — it's the package boundary, not an internal convenience barrel:
// my-lib/index.js — package entry point, this is fine
export { createClient } from "./client.js";
export { parseConfig } from "./config.js";The distinction: a package entry point defines what external consumers see. A subdirectory barrel is internal convenience that adds indirection without value.
Why this matters — social contract vs. language contract:
Barrel exports are unenforceable within a project. Any developer can always import directly from the source file — there is no mechanism to require they use the barrel instead. This creates two sources of truth for every export: the barrel and the source file. IDEs will autocomplete both paths, and over time a codebase accumulates a mix of import from "./services" and import from "./services/user.js" with no way to converge.
Package entry points are different. Node.js exports field in package.json restricts which paths external consumers can resolve — the runtime will throw on unauthorized deep imports. That's a language contract, not a social one, which is why barrel files at the package boundary work: they can actually be enforced.
Avoid Circular Dependencies
Circular imports create initialization order bugs. If module A imports B and B imports A, one of them will see an incomplete module.
Fix patterns:
- Extract shared code into a third module
- Merge tightly coupled modules
- Use dependency injection instead of direct imports
Side-Effect Imports
Import a module purely for its side effects only when necessary, and document why:
// Registers global polyfill
import "./polyfill.js";
// Initializes monitoring
import "./instrumentation.js";Side-effect imports should be rare. If you find many, reconsider the architecture.
JavaScript Objects and Arrays
Object/array patterns, destructuring, iteration, classes, and immutability.
Objects
Literal Syntax
Always use literal syntax. Never use constructors:
// Bad
const obj = new Object();
const arr = new Array();
// Good
const obj = {};
const arr = [];Shorthand
Use method shorthand, property shorthand, and computed property names:
const name = "Alice";
const key = "role";
const user = {
name, // property shorthand
[key]: "admin", // computed property
greet() { return "hello"; }, // method shorthand
};Shallow Copy with Spread
Use spread for shallow copies. Never mutate arguments or shared objects:
// Shallow copy
const copy = { ...original };
// Merge
const merged = { ...defaults, ...overrides };
// Omit properties via rest
const { password, ...safeUser } = user;Prefer spread over Object.assign().
Property Access
Use dot notation for static properties, brackets for dynamic:
user.name; // static
user[dynamicKey]; // dynamicUse Object.hasOwn(obj, key) instead of obj.hasOwnProperty(key):
// Bad — can be shadowed or fail on null-prototype objects
if (obj.hasOwnProperty("key")) { ... }
// Good
if (Object.hasOwn(obj, "key")) { ... }Don't Mutate Prototypes
Never extend built-in prototypes (Array.prototype, Object.prototype, etc.). Use utility functions or subclasses instead.
Arrays
Prefer Functional Methods
Use map, filter, reduce, find, some, every, flatMap over manual loops for data transformation:
// Bad — imperative
const active = [];
for (let i = 0; i < users.length; i++) {
if (users[i].active) active.push(users[i]);
}
// Good — declarative
const active = users.filter((u) => u.active);Array Method Rules
- Always return in
map,filter,reducecallbacks. - Use
Array.from(arrayLike)for array-like objects (not spread). - Use
Array.from(iterable, mapFn)instead of[...iterable].map(mapFn)— avoids an intermediate array.
Spread for Copies
const copy = [...original];
const combined = [...arr1, ...arr2];Immutable Updates
Don't mutate arrays — return new ones:
// Bad
items.push(newItem);
items.splice(index, 1);
// Good
const added = [...items, newItem];
const removed = items.filter((_, i) => i !== index);
const updated = items.map((item) =>
item.id === target.id ? { ...item, ...changes } : item
);for...of for Side Effects
When the loop body has side effects (not producing a new array), use for...of:
for (const item of items) {
await process(item);
}Don't use for...in on arrays — it iterates string keys including inherited properties.
Destructuring
Object Destructuring
Use destructuring to extract properties from objects:
// Bad
const name = user.name;
const email = user.email;
// Good
const { name, email } = user;
// With rename
const { name: userName, email: userEmail } = user;
// With defaults
const { role = "user", active = true } = options;
// Nested
const { address: { city } } = user;Parameter Destructuring
Destructure directly in function signatures:
// Good — clear which properties are used
function formatUser({ name, email, role = "user" }) {
return `${name} <${email}> (${role})`;
}Array Destructuring
const [first, second, ...rest] = items;
const [, , third] = items; // skip elements
// Swap
[a, b] = [b, a];Object Over Array for Return Values
When returning multiple values, prefer objects — callers don't depend on order:
// Bad — order-dependent
function getRange() { return [min, max]; }
const [min, max] = getRange();
// Good — order-independent
function getRange() { return { min, max }; }
const { min, max } = getRange();Classes
ES Classes Only
Use class syntax. Never use function constructors or prototype manipulation:
class Animal {
#name; // private field
constructor(name) {
this.#name = name;
}
get name() { return this.#name; }
speak() {
return `${this.#name} makes a sound`;
}
}
class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}Class Guidelines
- Prefer composition over inheritance. Use inheritance only for true "is-a" relationships.
- Use `#private` fields for encapsulation — not
_convention. - Methods can return `this` for fluent/chainable APIs.
- No empty constructors. If the constructor only calls
super(), omit it. - Static methods for operations that don't need instance state.
When Not to Use Classes
Don't force classes when plain functions and objects suffice. A class with one method is usually a function in disguise:
// Unnecessary class
class Validator {
validate(data) { return schema.parse(data); }
}
// Just a function
function validate(data) { return schema.parse(data); }Iteration
Choosing the Right Loop
- Transform data → new array →
.map() - Filter items →
.filter() - Accumulate to single value →
.reduce() - Find first match →
.find()/.findIndex() - Check condition →
.some()/.every() - Side effects on each item →
for...of - Async sequential processing →
for...ofwithawait - Object keys/values →
Object.entries()+for...of
Object Iteration
// Entries — most versatile
for (const [key, value] of Object.entries(obj)) { ... }
// Keys only
for (const key of Object.keys(obj)) { ... }
// Values only
for (const value of Object.values(obj)) { ... }Map and Set
Use Map for key-value collections where keys are not strings, or where insertion order matters. Use Set for unique value collections:
const cache = new Map();
cache.set(objectKey, value);
const unique = new Set(items);
const deduped = [...new Set(items)];Never use plain objects as maps when keys are user-provided — use Map to avoid prototype pollution.
Generators
Use generators for lazy sequences and custom iterables:
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
for (const n of range(0, 10, 2)) {
console.log(n); // 0, 2, 4, 6, 8
}yield* delegates to another iterable:
function* concat(...iterables) {
for (const iter of iterables) {
yield* iter;
}
}