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 ascreateEvent
which creates a new event object orcreateAttemptResponse
which creates a new attempt response object).getThing()
- Fetches something from some data source (such asgetEvent
which retrieves an event from the database orgetAttemptStatus(attempt)
which returns a status property from a provided attempt object).
Naming Variables
isSomething
- when a variable can only betrue
orfalse
, prefix it withis
to indicate that it is a boolean.someEl
- when storing a DOM or JSX element, suffix a variable withEl
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 whereasvar
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:
- Is the score inserted correctly?
- Is the event created correctly?
- 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.