Link Search Menu Expand Document

JavaScript Coding Style Guidelines

As the name suggests, these are guidelines rather than rules. Follow them when possible and especially when starting a new project, but if a project existed prior to the institution of these guidelines and has guidelines or rules that differ, prefer those.

Table of Contents

Linting

Linting refers to using some kind of tool or process to evaluate your code against a standard and flag any departures from that standard along with potential syntax issues.

Use a linter of some kind. ESLint is extremely configurable, and can use or extend presets if desired. It’ll even lint JSX. Typically linters are listed in the devDependency area of your project’s package.json file. Follow your chosen linter’s instructions for setup and usage.

Having an extension/plugin for your editor or IDE that uses the same rules as your project’s linter can be helpful, but at minimum the linter should be required to pass when making commits.

Exceptions

There will inevitably be cases which require you to write code that will not pass your linting conditions. In cases such as this, you can use a comment to disable the linter for certain circumstances. Only disable your linter for individual lines - never (almost) for entire files.

For example:

Do not:

// (Beginning of file)
/* eslint-disable no-console */

Do:

// eslint-disable-next-line no-console
expect(console.error).not.toHaveBeenCalled()

Or do:

structure.mockReturnValue(undefined) //eslint-disable-line no-undefined

It is possible for there to be exceptions for the exceptions - prefer next-line or in-line disable comments, but if doing so would result in excessive comments and the rule being disabled is limited enough, it may be reasonable to consider disabling a rule for an entire file.

Code Formatting

It may also be a good idea to use a code formatting tool such as Prettier to enforce style guidelines. Tools like prettier are used to automatically adjust code formatting to match a configurable set of rules. Much like your linter, your code formatting tool will likely be listed in the devDependency area of your project’s package.json file. Again, follow your chosen tool’s instructions for setup and usage.

Having an extension/plugin for your editor or IDE that uses the same code formatting tool/rules as your project (such as Prettier) can be helpful, but at minimum it is a good idea to add a pre-commit step that checks to see if the code formatter needs to be run in order for the code to conform to your project’s guidelines.

Naming Conventions

JavaScript typically follows Java function/variable/class/etc. naming conventions. In summary:

  • UpperCamelCase for class names.
  • lowerCamelCase for function/method/variable names.
  • SCREAMING_SNAKE_CASE for constants.

Naming Functions/Methods

Functions should be named according to what they do, starting with verbs where possible. For example:

  • createThing() - Creates and returns a new structured object (such as createEvent which creates a new event object or createAttemptResponse which creates a new attempt response object).
  • getThing() - Fetches something from some data source (such as getEvent which retrieves an event from the database or getAttemptStatus(attempt) which returns a status property from a provided attempt object).

Naming Variables

  • isSomething - when a variable can only be true or false, prefix it with is to indicate that it is a boolean.
  • someEl - when storing a DOM or JSX element, suffix a variable with El to indicate that it is an element that appears on the page.

Variables

var, let, and const

When defining variables:

  • Use const whenever possible - if a variable is only going to be set once, or when defining an array or an object.
  • Use let if a variable can potentially be changed.
  • Use var as little as possible, preferably not at all.

    There are special conditions around how var variables are scoped that make it unpredictable - let is scoped to the nearest closing block whereas var is scoped to the nearest function block.

When defining multiple variables prior to their use, always define them individually on their own lines.

For example:

Do not:

let foo, bar

Do:

let foo
let bar

Mutability (let vs. const)

Determining when to use const instead of let isn’t always immediately clear. Consider the mutability of the variable you’re defining. In summary: how will the variable be changing after it is defined?

If the variable itself may change after being defined (making it mutable), always define the variable with let. This includes adjustments to a value or the value being overwritten entirely.

For example:

// defined with 'let' - this variable is mutable
let someVariable = 0

// value is overwritten
// consider that this line is a shortcut for "someVariable = someVariable + 1"
someVariable++

// value is overwritten
someVariable = 2

If the variable itself will not change after being defined (making it immutable), but some property of the variable may change (a key in an object or an index in an array, for example), define the variable with const.

For example:

// defined with 'const' - these variables are immutable
const someObject = { key1: 'value1' }
// "someObject = { key2: 'value2' }" would cause an error, as the variable itself is immutable
const someArray = [1, 2, 3]
// "someArray = [4, 5, 6]" would cause an error, as the variable itself is immutable

// the object itself is immutable, but as an object its properties are not
someObject.key1 = 'value2'

// const objects can also have properties added or removed
someObject.key2 = 'value3'
delete someObject.key1

// the array itself is immutable, but its contained elements are not
someArray[0] = 4
someArray[1] = 5
someArray[2] = 6

// const arrays can also have elements added or removed
someArray.push(7)
someArray.shift()

Initial Value (null vs. undefined)

If it’s possible that a variable won’t be set or that a function could not return an expected value, consider setting the variable’s value to null or returning null instead. Using null is purposeful - it indicates that a value was set to null intentionally or that a value was intentionally not found, whereas undefined could mean that something did not execute properly or that a value was not found unintentionally.

For example:

Do not:

let foo = getFoo()

getFoo() {
	let myValue // myValue is not initialized (it is undefined)
	//...
	return myValue // may return undefined if myValue is never set
}

Also do not:

let bar = getBar(x)

getBar(a) {
	if ( ! a) return // returns undefined
}

Do:

let foo = getFoo()
let bar = getBar(x)

getFoo() {
    let myValue = null // initially set to null
    //...
    // will at least return null if not set within the code
    return myValue
}

getBar(a) {
    // could also return an error here, just not 'undefined'
    if ( ! a) return null
}

If the function return is not being used to set a value anywhere, it’s fine to return naturally without forcing null.

For example:

onClick(event) {
    // returning 'null' is unnecessary, if nothing is being set to
    //  the return value of 'onClick'
    if ( ! someEl.contains(event.target)) return
}

Function Declarations

When possible, use arrow functions (() => { ... }) instead of the older function() { ... } syntax. Keep in mind that functions will be scoped differently depending on which syntax is used.

For example:

Do not:

function doSomething(thingIn) {
    // ...
    return thingOut
}

Do:

const doSomething = thingIn => {
    // ...
    return thingOut
}

In situations where using the function() { ... } syntax is either necessary or significantly more convenient than using the () => { ... } syntax, it is generally fine to do so intentionally. Consider using a comment to explain intention in such scenarios.

Indentation

Never indent a line using spaces. Initial indentation should only ever use tabs.

Mid-Line Alignment

Using spaces to align code mid-line for readability is permissible, within reason, but try to only do it for logically grouped blocks of code

For example:

const aVarName             = 1
const anotherVarName       = 2
const anotherLongerVarName = 3

Curly Brace Placement

Curly braces should be placed on the same line as the block of code they are opening.

For example:

Do not:

if (condition)
{
    // ...
}

Do:

if (condition) {
    // ...
}

New Lines

Use new lines to separate code by logical blocks and/or to generally improve readability.

For example:

Do not:

onClick(event) {
    if ( ! event.target.contains(somEl)) return
    let new Counter = this.state.counter + 1
    Dispatcher.trigger('clicked', newCounter)
    this.setState({ counter: newCounter })
}

Do:

onClick(event) {
    if ( ! event.target.contains(someEl)) return

    let newCounter = this.state.counter + 1

    Dispatcher.trigger('clicked', newCounter)
    this.setState({ counter: newCounter })
}

White Space

Operators

Add a space on either side of all operators (+, -, =, ===, etc.).

Do not:

const aNumber=0
const anotherNumber=1+2
const isSomething=aNumber===anotherNumber

Do:

const aNumber = 0
const anotherNumber = 1 + 2
const isSomething = aNumber === anotherNumber

Conditional Statements

Add a space between conditional statements (if, else, for, switch, etc.) and their opening parenthesis, and between any statement’s closing parenthesis and its corresponding curly brace.

For example:

Do not:

if(condition){
    //...
}

Do:

if (condition) {
    //...
}

Add an additional space on either side of a negation (!) in conditionals for readability.

Do not:

if (!condition) {
    //...
}

Do:

if ( ! condition) {
    //...
}

Ternary Statements

Add a space on either side of ternary characters (? and :).

Do not:

const someValue = someOthervalue?100:200

Do:

const someValue = someOtherValue ? 100 : 200

Trailing White Space

When possible do not leave any trailing white space on a line. If possible use an editor extension/plugin to highlight trailing whitespace for easier detection and removal.

Blank Line at End of File

It is common practice to leave a single blank line at the end of a file - some code guidelines even mandate this, and some code formatters (such as Prettier) or even editors themselves (natively or with extensions/plugins) will enforce it automatically.

Line Length

Many coding standards across many languages typically suggest that any given line of code be no longer than 80 or 90 characters. This can potentially be enforced by a code formatter, but it ultimately unimportant. Javascript is fairly lenient with regards to writing longer statements across multiple lines, however, and while it is not necessary to invest significant time and energy in maintaining line lengths under 90 characters, do try to be mindful of long lines. This is most easily accomplished by breaking to a new line on a comma when interacting with objects, arrays, or function arguments.

For example:

Do not:

const anObjectWithManyProperties = { anObjectProperty: 'aValue', anotherObjectProperty: 'anotherValue', yetAnotherObjectProperty: 'yetAnotherValue' }

const anotherValue = someFunction(aFunctionArgument, anotherFunctionArgument, yetAnotherFunctionArgument)

Do:

const anObjectWithManyProperties = {
    anObjectProperty: 'aValue',
    anotherObjectProperty: 'anotherValue',
    yetAnotherObjectProperty: 'yetAnotherValue'
}

const anotherValue = someFunction(aFunctionArgument,
    anotherFunctionArgument,
    yetAnotherFunctionArgument
)

Semicolons

Outside of loop declarations and some other specific scenarios, semicolons are not required in valid JavaScript code, as JavaScript features automatic semicolon insertion as a convenience. If possible, configure your linter and code formatting tools to flag and/or strip them.

Do not:

let someValue = 100;

for (let i = 0; i <= 100; i++) {
    someValue += i;
}

return someValue;

Do:

let someValue = 100

for (let i = 0; i <= 100; i++) {
    someValue += i
}

return someValue

It’s important to note that while this is largely safe to do, not terminating statements with semicolons can occasionally lead to unexpected behavior or bugs, particularly when code is minified and concatenated. There are certain structures which, when used without the safety of semicolons (such as IIFEs, which will come up again later), are especially error-prone.

This guideline especially is very much subject to opinion - it can be argued that the absence of semicolons has a negative impact on code clarity and readability.

Checking Conditions

Statements (if/else vs. ternary vs. switch)

if/else statements for variable assignment should be avoided if a small in-line assignment or ternary could be used instead

For example, preferring a small in-line assignment:

Do not:

let isCorrect
if (score === 100) {
    isCorrect = true
} else {
    isCorrect = false
}

Do:

let isCorrect = score === 100

Or a ternary:

Do not:

let className

if (score === 100) {
    className = 'is-correct'
} else {
    className = 'is-not-correct'
}

Do:

let className = score === 100 ? 'is-correct' : 'is-not-correct'

Ternaries should never be nested within other ternaries, however.

Do not:

let className = score === 100 ? 'is-correct' : (score === 0 ? 'is-not-correct' : 'is-partially-correct')

Sometimes it’s unavoidable, but where possible do not nest if/else control structures - prefer else if instead.

For example:

Do not:

let className

if (score === 100) {
    className = 'is-correct'
} else {
    if (score === 0) {
        className = 'is-not-correct'
    } else {
        className = 'is-partially-correct'
    }
}

Do:

let className

if (score === 100) {
    className = 'is-correct'
} else if (score === 0) {
    className = 'is-not-correct'
} else {
    className = 'is-partially-correct'
}

When multiple results are possible based on a single condition, switch statements are generally preferable over multiple uses of if/else. In the previous examples, since the only thing being tested was the single score variable, a switch would be preferable.

For example:

let className

switch (score) {
    case 100:
        className = 'is-correct'
        break

    case 0:
        caseName = 'is-not-correct'
        break

    default:
        caseName = 'is-partially-correct'
        break
}

When using if/else statements, try to reduce them to their simplest form.

For example, the following block:

if (a && b) {
    if (c) {
        // 1
    } else if (d) {
        // 2
    }
} else if (b) {
    // 3
}

Would be more readable as:

if (a && b && c) {
    // 1
} else if (a && b && d) {
    // 2
} else if (b) {
    // 3
}

It could potentially also use early returns, and be even more readable as:

if ( ! b) return

if (a && c) {
    // 1
} else if (a && d) {
    // 2
} else {
    // 3
}

Condition Order

Try to keep conditions in logical order and avoid ‘Yoda conditions’, where the value you’re comparing against comes before the thing you’re comparing.

For example:

Do not:

if (1 <= nodes.size) { ... }

Do:

if (nodes.size >= 1) { ... }

Early Returns

As mentioned in the example above, it is better to return early when possible than to execute code after determining that it will be unnecessary. For example, the previous example above could also be written as:

if ( ! b) return

if (a && c) return 1

if (a && d) return 2

return 3

Best cases and exceptional cases should be checked first to return as early as possible.

For example:

Do not:

if (status !== null) {
    // ... do something
} else {
    return false
}

Also do not:

if (status !== null) {
    // ... do something
}

return false

Do:

if (status === null) return false

// ... do something

As in the above example, check any required conditions first and only execute the main logic of a function if those conditions are met. This makes it easier for other developers to see the expectations of a function and to see the main logic of that function without having to parse large or unnecessary if/else chains.

Functions and Testability

To make code more readable and more testable, it is better to have multiple smaller functions than it is to have one large function. When possible, functions should perform a single task rather than multiple tasks. Large tasks can be accomplished by stringing together multiple functions.

For example:

Do not:

updateScore(score, attempt) {
    // insert score record into database
    db.query(`INSERT INTO scores(score) VALUES($[score])`, { score })

    // create score event
    const event = {
        score,
        userId: currentUser.id,
        attemptId: attempt.id
        // ... etc.
    }

    // insert score event into database
    db.query(`INSERT INTO events(event) VALUES($[event])`, { event })
}

Testing this single method requires testing three things:

  1. Is the score inserted correctly?
  2. Is the event created correctly?
  3. Is the event inserted correctly?

Instead, this single function should call three individual functions, each of which does one of these things.

For example:

insertScore(score) {
    db.query(`INSERT INTO scores(score) VALUES($[score])`, { score })
}

createScoreEvent(score, attempt) {
    const event = {
        score,
        userId: currentUser.id,
        attemptId: attempt.id
        // ... etc.
    }
}

insertScoreEvent(event) {
    db.query(`INSERT INTO events(event) VALUES($[event])`, { event })
}

updateScore(score, attempt) {
    insertScore(score)
    insertScoreEvent(createScoreEvent(score, attempt))
}

Here, when we are testing updateScore we only need to test that insertScore, createEvent and insertEvent are being called as we expect them to be. We can then develop additional tests for the three individual methods.

Code Comments and Function Names

Ideally, function names should be clear and code should be self-documenting when possible. Code comments are always recommended and preferred with one exception - when the comment is redundant due to the code being self-evident.

Take the following example:

Do not:

getData(arr) {
    const values = {
        largest: null,
        highest: null,
        lowest: null
    }

    if (arr.length === 0) return values
    // ...
}

// set score to largest
let score = getData().largest

Here, there are two things to note. First: it is not clear what type of data is being returned by getData. Additionally, arr is not a helpful variable name as it only suggests that it is an array, but does nothing to suggest what the array should contain. Second: the comment in this case is redundant, as the code itself clearly suggests that the value of score will be set to whatever the value of largest is as returned by the function.

Do:

getAttemptsScoreSummary(attempts) {
    const scoreSummary = {
        largest: null,
        highest: null,
        lowest: null
    }

    // return default summary if no attempts are provided
    if (attempts.length === 0) return scoreSummary

    // ...
}

let largestAttemptScore = getAttemptScoreSummary(attempts).largest

After the change, the variable and function names add clarity to what the function does and what it expects. The comment is in this case not critical, but does add some context to the intention of the early return.

Block Comment vs. Line Comment

The decision to use a block comment instead of multiple single-line comments is ultimately up to personal preference. Consider the previous guideline regarding line length - a comment that would result in a line being of significant length could be a block comment, or multiple single-line comments.

For example:

Do not:

// this is a long explanation of some unusual or difficult-to-understand code that a function is using which is not self-documenting, thus requiring more context
someUnusualCodeFunction() {
    // ...
}

Do:

/*
this is a long explanation of some unusual or
difficult-to-understand code that a function
is using which is not self-documenting, thus
requiring more context
*/
someUnusualCodeFunction() {
    // ...
}

Or do:

// this is a long explanation of some unusual or
//  difficult-to-understand code that a function
//  is using which is not self-documenting, thus
//  requiring more context
someUnusualCodeFunction() {
    // ...
}

Again, the decision between a block comment or multiple single-line comments is ultimately one of personal preference. However, consider how many lines a comment requires to properly convey the necessary context. When writing block/multi-line comments try not to let the commentary make a line longer than 90 characters, and also try not to let the commentary extend too far past the right edge of the code it is explaining.

Avoid @todo comments

It’s common to use comments containing some permutation of todo to indicate that additional attention is necessary in order to polish, finalize, or otherwise improve code. In JavaScript, specifically in JSDoc, this is done by including @todo in a comment.

Using @todo comments is fine during development, but any lingering to-dos should be addressed prior to code being finalized and submitted in pull requests. If a @todo hasn’t been addressed by the time development on a feature or issue resolution is complete, consider creating an issue and referring to that location in the code in the issue instead.

For example:

Do not:

// @todo - move this method somewhere else
StylableText.createFromElement = () => { ... }

Repetition

Duplicate code can easily cause issues for yourself, or especially for future developers. If code doing the same thing exists in two locations, it’s easy to forget about or miss one copy when updating the other. Follow the principal of DRY - move duplicated code into a function that can be called in all the locations where the code was originally running, or reorganize code so that duplicated code is not necessary.

For example:

Do not:

if (some.thing !== null) {
    let myThing = getThing(some.thing)
    myThing.init()
    myThing.color = 'red'
} else if (example !== null) {
    let myThing = getThing(example)
    myThing.init()
    myThing.color = 'red'
}

return myThing

Do:

// move duplicate code into a function that can be called multiple times
createARedThing(id) {
    const myThing = getThing(thingId)

    myThing.init()
    myThing.color = 'red'

    return myThing
}

createARedThingWhenPossible() {
    // ...
    if (some.thing !== null) {
        return createARedThing(some.thing)
    } else if (example !== null) {
        return createARedThing(example)
    }

    return null
}

Or do:

// reorganize code so that the duplication is not necessary
let thingId = null

if (some.thing !== null) {
    thingId = some.thing
} else if (example !== null) {
    thingId = example
}

if ( ! thingId) return null

const myThing = getThing(thingId)
myThing.init()
myThing.color = 'red'

return myThing

This concept of avoiding repetition does not necessarily apply to tests which, by their nature, tend to require significant repetition in order to adequately test all possible conditions.

Cleverness

Clever code is only clever if everyone who reads it understands what it’s doing. Take the following example:

Do not:

;(function fade() {
    let val = parseFloat(el.style.opacity)
    if ( ! ((val += 0.1) > 1)) {
        el.style.opacity = val
        window.requestAnimationFrame(fade)
    }
})()

While the function itself is not difficult to understand, the surrounding structure is unsual. Without prior knowledge of the trick being used here, a developer would have no idea what to make of this code.

While it’s generally a better idea to simply avoid using tricky code like this in the first place, at the very least leave a comment explaining what the code is doing.

Do:

// immediately executes a function to increase an element's opacity and
//  request an animation from the browser if the element becomes fully opaque
// leading semicolon to avoid potential syntax errors
;(function fade() {
    let val = parseFloat(el.style.opacity)
    if ( ! ((val += 0.1) > 1)) {
        el.style.opacity = val
        window.requestAnimationFrame(fade)
    }
})()

In cases where a trick like this is used more than once across a code base, it can be easy to forget to leave a comment explaining the more unusual aspects of the code. Again - avoid using unusual code when possible, but if it is necessary or more convenient than code that is self-documenting then try to contain it to a single function reused in multiple places or, at the very least, leave comments to explain the unusual code every time it is used.

The above example makes use of two tricks, one more common than the other. First, it is using a self-executing anonymous function or IIFE - an anonymous function that is executed immediately after its definition. Second, it is using a leading semicolon as a defensive measure to ensure there are no syntax errors when this code is run - this is especially important if surrounding code is not using semicolons to terminate statements.