JavaScript Clean Coding Best Practices

Written by: András Tóth

This article was originally published on RisingStack's blog by András Tóth. With their kind permission, we’re sharing it here for Codeship readers.

Writing clean code is what you must know and do in order to call yourself a professional developer. There is no reasonable excuse for doing anything less than your best.

In this blog post, we will cover general clean coding principles for naming and using variables and functions, as well as some JavaScript-specific clean coding best practices.

“Even bad code can function. But if the code isn’t clean, it can bring a development organization to its knees.” — Robert C. Martin (Uncle Bob)

First of all, what does clean coding mean?

Clean coding means that in the first place you write code for your later self and for your coworkers and not for the machine. Your code must be easily understandable for humans.

You know you are working on a clean code when each routine you read turns out to be pretty much what you expected.

JavaScript Clean Coding Best Practices

Now that we know what every developer should aim for, let’s go through the best practices!

How should I name my variables?

Use intention-revealing names and don't worry if you have long variable names instead of saving a few keyboard strokes.

If you follow this practice, your names become searchable, which helps a lot when you do refactors or you are just looking for something.

// DON'T
let d
let elapsed
const ages = arr.map((i) => i.age)
// DO
let daysSinceModification
const agesOfUsers = users.map((user) => user.age)

Make meaningful distinctions and don't add extra, unnecessary nouns to the variable names, like its type (Hungarian notation).

// DON'T
let nameString
let theUsers
// DO
let name
let users

Make your variable names easy to pronounce, because it takes less effort for the human mind to process. When you are doing code reviews with your fellow developers, these names are easier to reference.

// DON'T
let fName, lName
let cntr
let full = false
if (cart.size > 100) {
  full = true
}
// DO
let firstName, lastName
let counter
const MAX_CART_SIZE = 100
// ...
const isFull = cart.size > MAX_CART_SIZE

In short, don't cause extra mental mapping with your names.

How should I write my functions?

Your functions should do one thing only on one level of abstraction.

Functions should do one thing. They should do it well. They should do it only. — Robert C. Martin (Uncle Bob)

// DON'T
function getUserRouteHandler (req, res) {
  const { userId } = req.params
  // inline SQL query
  knex('user')
    .where({ id: userId })
    .first()
    .then((user) => res.json(user))
}
// DO
// User model (eg. models/user.js)
const tableName = 'user'
const User = {
  getOne (userId) {
    return knex(tableName)
      .where({ id: userId })
      .first()
  }
}
// route handler (eg. server/routes/user/get.js)
function getUserRouteHandler (req, res) {
  const { userId } = req.params
  User.getOne(userId)
    .then((user) => res.json(user))
}

After you write your functions properly, you can test how well you did with CPU profiling, which helps you find bottlenecks.

Use long, descriptive names

A function name should be a verb or a verb phrase, and it needs to communicate its intent, as well as the order and intent of the arguments.

A long descriptive name is way better than a short, enigmatic name or a long descriptive comment.

// DON'T
/**
 * Invite a new user with its email address
 * @param {String} user email address
 */
function inv (user) { /* implementation */ }
// DO
function inviteUser (emailAddress) { /* implementation */ }  

Avoid long argument list

Use a single object parameter and destructuring assignment instead. This also makes handling optional parameters much easier.

// DON'T
function getRegisteredUsers (fields, include, fromDate, toDate) { /* implementation */ }
getRegisteredUsers(['firstName', 'lastName', 'email'], ['invitedUsers'], '2016-09-26', '2016-12-13')
// DO
function getRegisteredUsers ({ fields, include, fromDate, toDate }) { /* implementation */ }
getRegisteredUsers({
  fields: ['firstName', 'lastName', 'email'],
  include: ['invitedUsers'],
  fromDate: '2016-09-26',
  toDate: '2016-12-13'
})

Reduce side effects

Use pure functions without side effects, whenever you can. They are really easy to use and test.

// DON'T
function addItemToCart (cart, item, quantity = 1) {
  const alreadyInCart = cart.get(item.id) || 0
  cart.set(item.id, alreadyInCart + quantity)
  return cart
}
// DO
// not modifying the original cart
function addItemToCart (cart, item, quantity = 1) {
  const cartCopy = new Map(cart)
  const alreadyInCart = cartCopy.get(item.id) || 0
  cartCopy.set(item.id, alreadyInCart + quantity)
  return cartCopy
}
// or by invert the method location
// you can expect that the original object will be mutated
// addItemToCart(cart, item, quantity) -> cart.addItem(item, quantity)
const cart = new Map()
Object.assign(cart, {
  addItem (item, quantity = 1) {
    const alreadyInCart = this.get(item.id) || 0
    this.set(item.id, alreadyInCart + quantity)
    return this
  }
})

Organize your functions in a file according to the stepdown rule

Higher level functions should be on top and lower levels below. It makes it natural to read the source code.

// DON'T
// "I need the full name for something..."
function getFullName (user) {
  return `${user.firstName} ${user.lastName}`
}
function renderEmailTemplate (user) {
  // "oh, here"
  const fullName = getFullName(user)
  return `Dear ${fullName}, ...`
}
// DO
function renderEmailTemplate (user) {
  // "I need the full name of the user"
  const fullName = getFullName(user)
  return `Dear ${fullName}, ...`
}
// "I use this for the email template rendering"
function getFullName (user) {
  return `${user.firstName} ${user.lastName}`
}

Query or modification

Functions should either do something (modify) or answer something (query), but not both.

Everyone likes to write JavaScript differently. What to do?

As JavaScript is dynamic and loosely typed, it is especially prone to programmer errors.

Use project or company-wide linter rules and formatting style.

The stricter the rules, the less effort will go into pointing out bad formatting in code reviews. It should cover things like:

  • consistent naming

  • indentation size

  • whitespace placement

  • semicolons

The standard JS style is quite nice to start with, but in my opinion, it isn't strict enough. I can agree with most of the rules in the Airbnb style.

How to write nice async code?

Use Promises whenever you can. Promises are natively available from Node 4. Instead of writing nested callbacks, you can have chainable Promise calls.

// AVOID
asyncFunc1((err, result1) => {
  asyncFunc2(result1, (err, result2) => {
    asyncFunc3(result2, (err, result3) => {
      console.lor(result3)
    })
  })
})
// PREFER
asyncFuncPromise1()
  .then(asyncFuncPromise2)
  .then(asyncFuncPromise3)
  .then((result) => console.log(result))
  .catch((err) => console.error(err))

Most of the libraries out there have both callback and Promise interfaces; prefer the latter. You can even convert callback APIs to Promise-based one by wrapping them using packages like es6-promisify.

// AVOID
const fs = require('fs')
<p><p>function readJSON (filePath, callback) {
  fs.readFile(filePath, (err, data) => {
    if (err) {
      return callback(err)
    }
    try {
      callback(null, JSON.parse(data))
    } catch (ex) {
  callback(ex)
    }
  })
}
readJSON('./package.json', (err, pkg) => { console.log(err, pkg) })
// PREFER
const fs = require('fs')
const promisify = require('es6-promisify')
const readFile = promisify(fs.readFile)
function readJSON (filePath) {
  return readFile(filePath)
    .then((data) => JSON.parse(data))
}
readJSON('./package.json')
  .then((pkg) => console.log(pkg))
  .catch((err) => console.error(err))

The next step would be to use async/await (≥ Node 7) or generators with co (≥ Node 4) to achieve synchronous-like control flows for your asynchronous code.

const request = require('request-promise-native')
function getExtractFromWikipedia (title) {
  return request({
    uri: 'https://en.wikipedia.org/w/api.php',
    qs: {
      titles: title,
      action: 'query',
      format: 'json',
      prop: 'extracts',
      exintro: true,
      explaintext: true
    },
    method: 'GET',
    json: true
  })
    .then((body) => Object.keys(body.query.pages).map((key) => body.query.pages[key].extract))
    .then((extracts) => extracts[0])
    .catch((err) => {
      console.error('getExtractFromWikipedia() error:', err)
      throw err
    })
}
// PREFER
async function getExtractFromWikipedia (title) {
  let body
  try {
    body = await request({ /* same parameters as above */ })
  } catch (err) {
    console.error('getExtractFromWikipedia() error:', err)
    throw err
  }
const extracts = Object.keys(body.query.pages).map((key) => body.query.pages[key].extract)
  return extracts[0]
}
// or
const co = require('co')
const getExtractFromWikipedia = co.wrap(function * (title) {
  let body
  try {
    body = yield request({ /* same parameters as above */ })
  } catch (err) {
    console.error('getExtractFromWikipedia() error:', err)
    throw err
  }
const extracts = Object.keys(body.query.pages).map((key) => body.query.pages[key].extract)
  return extracts[0]
})
getExtractFromWikipedia('Robert Cecil Martin')
  .then((robert) => console.log(robert))

How should I write performant code?

In the first place, you should write clean code, then use profiling to find performance bottlenecks.

Never try to write performant and smart code first. Instead, optimize the code when you need to and refer to true impact instead of micro-benchmarks.

Although, there are some straightforward scenarios, like eagerly initializing what you can (eg, Joi schemas in route handlers, which would be used in every request and add serious overhead if recreated every time) and using asynchronous instead of blocking code.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.