ES6+ shorter
author: nicholaswmin, MIT License
Expression-based JavaScript
looks like:
// queue.js - queue with concurrency limit
class Queue extends Array {
enqueue = (...values) => (this.push(...values), this)
dequeue = () => this.shift()
peek = () => this.at(0)
race = async (fn, { max = 3 } = {}) => {
const exec = async (inactive, inflight = [], finished = []) => {
if (!inactive.length && !inflight.length)
return finished
const dispatch = elem => fn(elem).then(result => ({ result }))
const required = Math.min(max - inflight.length, inactive.length)
const starting = inactive.slice(0, required).map(dispatch)
const tracking = [ ...inflight, ...starting ],
resolved = await Promise.race(tracking)
return exec(
inactive.slice(required),
tracking.filter(p => p !== resolved),
[...finished, resolved.result]
)
}
return new Queue(...await exec([...this]))
}
}
// Usage
const uploads = await urls
.race(fetch, { max: 20 })
.sort(by('owner').desc())
- Layer functions by purpose
- Separate logic from side effects
- Skip needless intermediates
- Avoid nesting; use early returns
- Use newlines to group sections
- Avoid needless semis, parens or braces
- Prefer arrows & implicit return
- Avoid pointless exotic syntax
- Consider iteration over repetition
- Use 80 character lines for code
- Use 70 characters for comments
- Prefer expressions over statements
- Stay immutable where possible
- Avoid (most) imperative loops
- Chain methods over nesting calls
- Use built-in test runner
- Structure tests hierarchically
- Write focused tests
- Attach fixtures to test context
- Use context for test utilities
- Assert the minimum required
- Test for keywords, not sentences
ES modules are the JavaScript standard.
CommonJS is Node's legacy.
// ✅ ES modules
import { readFile } from 'fs/promises'
import { users, products } from './data.js'
import config from './config.js'
export const process = data => transform(data)
export const validate = input => check(input)
export default { process, validate }
// ❌ CommonJS
const fs = require('fs')
const { users } = require('./data')
module.exports = { process, validate }
Every dependency is a liability.
Prefer built-ins and writing small utilities.
// ✅ Use built-ins
const unique = [...new Set(items)]
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const pick = (obj, keys) =>
keys.reduce((acc, k) => ({ ...acc, [k]: obj[k] }), {})
// ❌ Don't install a package for one-liners
import uniq from 'lodash.uniq' // 4.4KB for [...new Set()]
import delay from 'delay' // 2.8KB for Promise + setTimeout
However, avoid DIY in production for domains that are:
- Too complex (game engines, CRDTs, date/time math)
- Too critical (cryptography, validation)
- Utility: Generic non-domain helpers
- Domain: Domain-specific helpers
- Orchestration: Usually the
main
of the module, program etc
// ✅ Utility functions
const gzip = file => compress(file, 'gzip', { lvl: 1 })
const type = file => ext => file.filename.includes(ext)
// ✅ Domain functions
const upload = async (file, n = 0) => {
const result = await s3.upload(file)
return result.error
? n <= 3
? upload(file, ++n)
: ignore(file)
: result
}
// ✅ Orchestration functions
const synchronize = async files => {
const timedout = delay(30000)
const eligible = files.filter(type('png')).map(gzip)
const uploaded = await Promise.all(eligible.map(upload))
const response = await Promise.race([uploaded, timedout])
return response === timedout
? ontimeout(eligible)
: normalize(response)
}
- Prefer pure functions over side effects
- Move side effects to boundaries
- Extract dependencies for easier mocking
// ✅ Testable - pure function
const discount = (price, pct) => price * (pct / 100)
// ✅ Testable - dependency injection
const notify = (user, emailer) => emailer.send(user.email, 'Welcome!')
// ❌ Hard to test - side effects
const applyDiscount = (price, percentage) => {
const discount = price * (percentage / 100)
updateDatabase(discount)
sendEmail(discount)
return discount
}
// ❌ Hard to test - hard-coded dependency
const notify = (user) => EmailService.send(user.email, 'Welcome!')
Extract dependencies when the function:
- Reaches outside your process space, e.g: network requests, filesystem
- Depends on uncontrollable factors (timers, accelerometers)
- Is slow or generally awkward to test
- Needs different implementations in different contexts
However, every indirection adds complexity.
Extract only when the benefit outweighs the cost.
// ✅ Simple operations don't need extraction
const formatName = (first, last) => `${first} ${last}`
// ❌ Needless indirection
const formatName = (first, last, formatter) => formatter.format(first, last)
Chain or inline unless the intermediate clarifies complex logic.
// ✅ No intermediates needed
const promote = users =>
users.map(review).filter(passed)
// ❌ Needless intermediates
const promote = users => {
const reviewed = users.map(review)
const eligible = reviewed.filter(passed)
return eligible
}
However, some functions naturally orchestrate others.
Don't mangle business logic for brevity—but don't make it
intentionally verbose either.
Early returns flatten code and eliminate edge cases upfront.
Each guard clause removes a level of indentation, making the
happy path obvious.
// ✅ Early return
onUpdate(fence) {
if (this.insulated)
return
if (!this.intersects(fence))
return this.electrocute()
// ... fancy sync logic
}
// ❌ Nested conditions
onUpdate(fence) {
if (!this.insulated) {
if (this.intersects(fence)) {
// ... fancy sync logic
} else {
this.electrocute()
}
}
}
However, don't use them defensively.
Fix the caller instead of guarding against bad inputs.
Split functions into visually-distinct sections.
Consider separating:
- Early returns/filtering
- Variable declarations
- Main logic
- Return statement
// ✅ Clear function structure
const synchronize = async files => {
const timedout = delay(30000)
const eligible = files.filter(valid).filter(type('png'))
const uploaded = await Promise.all(eligible.map(upload))
const response = await Promise.race([uploaded, timedout])
return response === timedout
? ontimeout(eligible)
: normalize(response)
}
// ❌ No separation
const synchronize = async files => {
const timedout = delay(30000)
const eligible = files.filter(valid).filter(type('png'))
const uploaded = await Promise.all(eligible.map(upload))
const response = await Promise.race([uploaded, timedout])
return response === timedout ? ontimeout(eligible) : normalize(response)
}
Name by role, not by type.
Choose single words when precise alternatives exist, however...
"There are only two hard things in Computer Science:
cache invalidation and naming things."
— HRM Queen Elizabeth II
// ✅ Good
users
active
name
overdue // more precise than isPaid
// ❌ Avoid
userList
isUserActive
firstName
lastName
isPaid // when overdue is clearer
Assume the surroundings provide the context.
// ✅ Context eliminates redundancy
const user = { name, email, age } // not userName, userEmail, userAge
const { data, status } = response // not responseData, responseStatus
// ❌ Redundant qualifiers
const user = { userName, userEmail, userAge }
const { responseData, responseStatus } = response
Group related properties under logical objects.
Avoid meaningless wrappers or technical groupings.
// ✅ Appropriate namespacing
const user = {
name: { first, last, full },
contact: { email, phone },
status: { active, verified }
}
const config = {
server: { port, host },
database: { url, timeout }
}
// ❌ Inappropriate namespacing
const user = {
data: { value: 'John' }, // Meaningless wrapper
info: { type: 'admin' }, // Generic grouping
props: { id: 123 } // Technical grouping
}
Minimal syntax, maximum signal.
// ✅ Minimal syntax
const name = 'Alice'
const double = x => x * 2
const greet = name => `Hello ${name}`
users.map(user => user.name)
if (subscribers.length)
throw new Error('Session is subscribed')
const user = { name, email, age }
// ❌ Unnecessary syntax
const name = 'Alice';
const double = x => { return x * 2; }
users.map((user) => user.name);
const user = { 'name': name, 'email': email };
Skip braces and return for single expressions.
// ✅ Implicit return
const double = x => x * 2
const getName = user => user.name
const sum = (a, b) => a + b
const makeUser = (name, age) => ({ name, age })
// ❌ Explicit return for single expressions
const double = x => { return x * 2 }
const getName = user => { return user.name }
const makeUser = (name, age) => { return { name, age } }
Choose clarity over cleverness.
Use language features when they improve readability, not just because.
// ✅ Clear intent
const isEven = n => n % 2 === 0
// ❌ Clever but unclear
const isEven = n => !(n & 1)
Two items? Maybe copy-paste.
Three or more? Use a loop for easier extension.
// ✅ Dynamic generation
return ['get', 'put', 'post', 'patch']
.reduce((acc, method) => ({ ...acc, [method]: send }), {})
// ❌ Manual enumeration
return { get: send, put: send, post: send, patch: send }
Avoid overuse, especially in tests; loops need mental parsing.
// ❌ Overabstracted test
['admin', 'user', 'guest'].forEach(role => {
test(`${role} access`, t => {
const result = checkAccess(role)
t.assert.strictEqual(result, role === 'admin')
})
})
// ✅ Clear, explicit tests
test('admin has access', t => {
t.assert.strictEqual(checkAccess('admin'), true)
})
test('user lacks access', t => {
t.assert.strictEqual(checkAccess('user'), false)
})
test('guest lacks access', t => {
t.assert.strictEqual(checkAccess('guest'), false)
})
70 soft limit, 80 hard limit.
Break at 50 for method chains or arguments.
// ✅ Under 50 characters - keep on one line
const double = arr => arr.map(x => x * 2)
const sum = arr => arr.reduce((a, b) => a + b)
// ✅ Over 50 characters - break into multiple lines
const process = data =>
data
.filter(x => x > 0)
.map(x => x * 2)
.reduce((a, b) => a + b)
Comments are a maintenance burden.
Reserve them for explaining "why", not "what".
// ✅ Explaining unusual approach
// Polling needed due to API limitations
while (!response.complete)
await request(query, { delay: 200 })
// ❌ Obvious comments
const total = prices.reduce((sum, price) => sum + price, 0) // sum prices
When converting data, think pipelines not procedures.
// ✅ Functional for transformations
const users = data
.filter(active)
.map(normalize)
.sort(by.name)
If it has identity and changes over time (User, Game, Session), make it a class.
// ✅ OOP for entities
class User {
constructor(name, role) {
this.name = name
this.role = role
}
evaluate = () => this.score > 80
promote = () => {
this.role = 'senior'
return this
}
}
Expressions return values and compose.
// ✅ Good: ternaries over if
const upload = async (file, n = 0) => {
const result = await s3.upload(file)
return result.err
? n <= 3
? upload(file, ++n)
: ignore(file)
: result
}
// ✅ Good: object lookup over switch/case
const handle = action => ({
create: () => save(data),
update: () => modify(data),
delete: () => remove(data)
})[action]?.() || cancel()
// ❌ Avoid
switch (action) {
case 'create':
return save(data)
case 'update':
return modify(data)
case 'delete':
return remove(data)
default:
return defaultAction()
}
[...arr, item]
not arr.push(item)
.
New values are easier to debug than mutations.
// ✅ Immutable
const add = (users, user) => [...users, user]
const update = (user, data) => ({ ...user, ...data })
// ❌ Mutating
const add = (users, user) => users.push(user)
const update = (user, data) => Object.assign(user, data)
prices.filter(discounted)
tells what's happening.
A for
loop makes you figure it out.
// ✅ Functional approach
const active = users.filter(user => user.active)
const names = users.map(user => user.name)
const total = prices.reduce((sum, price) => sum + price, 0)
// ✅ Sequential async operations
for (const file of files)
await fetch(file)
// ❌ Imperative loops
const active = []
for (let i = 0; i < users.length; i++) {
if (users[i].active) {
active.push(users[i])
}
}
Inside-out is hard. Top-to-bottom is natural.
// ✅ Multi-line chaining
const process = data =>
data
.filter(x => x > 0)
.map(x => x * 2)
.reduce((a, b) => a + b)
// ❌ Nested function calls
const process = data => reduce(map(filter(data, x => x > 0), x => x * 2), (a, b) => a + b)
animation.fadeIn().wait(300).slideOut()
Each method returns the object for the next.
// ✅ Method chaining
class Calculator {
constructor(val = 0) { this.val = val }
add = n => new Calculator(this.val + n)
mul = n => new Calculator(this.val * n)
get = () => this.val
}
// Usage: new Calculator(5).add(3).mul(2).get() // 16
// ✅ Builder pattern for async workflows
const query = select('name', 'email')
.from('users')
.where('active', true)
.where('role', 'admin')
.orderBy('name')
.limit(10)
// ✅ Domain-specific naming
const animation = animate(element)
.to({ opacity: 0, x: 100 })
.duration(300)
.easing('ease-out')
.then(() => element.remove())
Trust your code; no trust, write tests.
Definitely don't litter the place with guard clauses.
// ✅ Trust internal functions
const process = data =>
data
.filter(active)
.map(normalize)
// ❌ Defensive programming within internal functions
const process = data => {
if (!data || !Array.isArray(data)) return []
return data
.filter(item => item && active(item))
.map(item => item ? normalize(item) : null)
.filter(Boolean)
}
// ❌ Defensive early returns
const formatName = (first, last) => {
if (!first) return '' // Why is caller passing empty names?
if (!last) return '' // Fix the caller instead
return `${first} ${last}`
}
// ✅ Trust the caller
const formatName = (first, last) => `${first} ${last}`
Exception: External data must be validated.
Be specific:
- Wrong type?
TypeError
- Out of bounds?
RangeError
- Everything else?
Error
with a clear message
// ✅ Specific error types
if (typeof value !== 'number')
throw new TypeError(`Expected number, got ${typeof value}`)
if (value < 0 || value > 100)
throw new RangeError('Value must be between 0 and 100')
if (!process.env.API_KEY)
throw new Error('API_KEY missing')
// ❌ String errors
throw 'Invalid value'
// ❌ Generic errors
throw new Error('Invalid input')
Users add spaces.
Environment variables arrive as strings.
Clean everything at the borders.
// ✅ Environment variables
const port = parseInt(process.env.PORT || '3000', 10)
const env = (process.env.NODE_ENV || 'development').toLowerCase()
// ✅ Booleans
const enabled = /^(true|1|yes|on)$/i.test(process.env.ENABLED || '')
// ✅ Arrays from CSV
const hosts = (process.env.HOSTS || '')
.split(',')
.map(s => s.trim())
.filter(Boolean)
// ✅ User input
const email = (input || '').trim().toLowerCase()
// ❌ No normalization
const port = process.env.PORT // Could be undefined/string
const enabled = process.env.ENABLED // String, not boolean
Validation:
// ✅ Validate at boundaries
const handleRequest = req => {
if (!req.body?.userId)
throw new Error('userId required')
if (typeof req.body.userId !== 'string')
throw new TypeError('userId must be string')
return processUser(req.body.userId)
}
// ✅ Early validation
const createUser = data => {
if (!data.name) throw new Error('Name required')
if (!data.email) throw new Error('Email required')
// Trust internal processing
return save(normalize(data))
}
// ❌ Late validation
const processUser = data => {
const result = expensiveOperation(data)
if (!data.name) throw new Error('Invalid data') // Too late
return result
}
Since Node v19: node --test
.
Zero dependencies.
// ✅ Built-in test runner
// Run with: node --test
import { test } from 'node:test'
// ❌ External dependencies
import { describe, it } from 'jest'
import { expect } from 'chai'
Tests tell a story: component → scenario → expectation.
Nest them that way.
Write for non-technical readers—including future you.
You won't remember the context when it breaks.
// ✅ Hierarchical structure
test('#withdraw', async t => {
await t.test('amount within balance', async t => {
await t.test('disperses requested amount', async t => {
// assertion
})
})
await t.test('amount exceeds balance', async t => {
await t.test('throws appropriate error', async t => {
// assertion
})
})
})
// ❌ Flat structure
test('withdraw disperses amount when within balance', t => {})
test('withdraw throws when amount exceeds balance', t => {})
One test, one assertion, one failure reason.
// ✅ Granular tests
test('#withdraw', async t => {
await t.test('within balance', async t => {
await t.test('returns updated balance', async t => {
t.assert.strictEqual(await t.atm.withdraw(30), 70)
})
})
})
// ❌ Multiple assertions, redundant titles
test('ATM withdrawal when amount is within available balance should disperse the requested amount and update balance', async t => {
const initial = t.atm.balance
const result = await t.atm.withdraw(30)
t.assert.strictEqual(result, 70)
t.assert.strictEqual(t.atm.balance, 70)
t.assert.strictEqual(initial - 30, t.atm.balance)
t.assert.ok(t.atm.lastTransaction)
})
Attach test data directly to the context using
t.beforeEach(t => t.atm = new ATM(100))
.
Avoid external variables or imports for fixtures.
// ✅ Context fixtures
test('#withdraw', async t => {
t.beforeEach(t => t.atm = new ATM(100))
await t.test('disperses amount', async t => {
await t.atm.withdraw(30) // t.atm available
})
})
// ❌ External fixtures
let atm // External variable
beforeEach(() => {
atm = new ATM(100)
})
test('disperses amount', async t => {
await atm.withdraw(30)
})
Everything's on t
: t.assert
, t.mock
, t.beforeEach
.
t.mock
resets automatically between tests.
// ✅ Context utilities
test('#notify', async t => {
await t.test('sends email', async t => {
const emailer = t.mock.fn()
notify(user, { send: emailer })
t.assert.strictEqual(emailer.mock.calls.length, 1)
})
})
// ❌ Imported utilities
import { assert, mock } from 'node:test'
test('sends email', async t => {
const emailer = mock.fn()
// ...
assert.strictEqual(emailer.mock.calls.length, 1)
})
Don't test what you don't care about,
especially if the object is expected to be extended.
// ✅ Partial assertions
test('#user properties', async t => {
await t.test('has correct details', t => {
t.assert.partialDeepStrictEqual(t.user, {
name: 'Alice', role: 'admin'
})
})
})
// ❌ Full object matching
test('has correct details', t => {
t.assert.deepStrictEqual(t.user, {
id: '123',
name: 'Alice',
role: 'admin',
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
// ... all properties
})
})
Match keywords that survive rephrasing:
"Insufficient balance" → "You have insufficient funds"
However, pick keywords unique to the test:
- ✓
/insufficient/
- specific to this error - ✗
/balance/
or/invalid/
- too generic
Otherwise your test becomes junk.
// ✅ Flexible matching
test('#withdraw', async t => {
await t.test('exceeds balance', async t => {
await t.assert.rejects(() => t.atm.withdraw(150), {
name: 'Error', message: /insufficient/i
})
})
})
// ❌ Exact string matching
await t.assert.rejects(() => t.atm.withdraw(150), {
name: 'Error',
message: 'Insufficient funds: cannot withdraw 150 from balance of 100'
})
Complete test example:
test('#atm', async t => {
t.beforeEach(t => t.atm = new ATM(100))
await t.test('#withdraw', async t => {
await t.test('within balance', async t => {
await t.test('returns updated balance', async t => {
t.assert.strictEqual(await t.atm.withdraw(30), 70)
})
})
await t.test('exceeds balance', async t => {
await t.test('throws error', async t => {
await t.assert.rejects(() => t.atm.withdraw(150), {
name: 'Error', message: /insufficient/i
})
})
await t.test('preserves balance', async t => {
await t.assert.rejects(() => t.atm.withdraw(150))
t.assert.strictEqual(t.atm.balance, 100)
})
})
})
await t.test('#properties', async t => {
await t.test('has location and currency', t => {
t.assert.partialDeepStrictEqual(t.atm, {
location: 'Main Street', currency: 'USD'
})
})
})
})