Skip to content

Instantly share code, notes, and snippets.

@nicholaswmin
Last active July 18, 2025 16:48
Show Gist options
  • Save nicholaswmin/e27e0d3de353c3f6f49a5dbba96342f5 to your computer and use it in GitHub Desktop.
Save nicholaswmin/e27e0d3de353c3f6f49a5dbba96342f5 to your computer and use it in GitHub Desktop.
ES6+ style guide

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

foundation

code organization

naming

syntax

paradigms

functional

object-oriented

error handling

testing

Prefer ESM, avoid CommonJS

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 }

Avoid needless dependencies

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)

Layer functions by purpose

  • 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)
}

Separate logic from side effects

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

Skip needless intermediates

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.

Avoid nesting; use early returns

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.

Use newlines to group sections

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 concisely and precisely

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

Drop redundant qualifiers

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

Use namespacing for structure

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
}

Avoid needless semis, parens or braces

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 };

Prefer arrows & implicit return

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 } }

Avoid pointless exotic syntax

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)

Consider iteration over repetition

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

Use 80 character lines for code

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)

Use 70 characters for comments

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

Use functional programming for data flows

When converting data, think pipelines not procedures.

// ✅ Functional for transformations
const users = data
  .filter(active)
  .map(normalize)
  .sort(by.name)

Use object-orientation for stateful entities

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
  }
}

Prefer expressions over statements

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

Stay immutable where possible

[...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)

Avoid (most) imperative loops

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])
  }
}

Chain methods over nesting calls

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)

Use method-chaining for fluent APIs

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

Avoid defensive programming

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.

Use appropriate error types

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

Normalize & validate external 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
}

Use built-in test runner

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'

Structure tests hierarchically

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 => {})

Write focused tests

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 fixtures to test context

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

Use context for test utilities

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

Assert the minimum required

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

Test for keywords, not sentences

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'
      })
    })
  })
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment