Skip to main content

Conventions for JavaScript code

The following conventions cover writing JavaScript code for Posos' front-end projects. This documentation is a list of rules for writing concise code that will be understandable by as many people as possible.

General conventions for JavaScript code

This section explains the general conventions to keep in mind while writing JavaScript code. The later sections will cover more specific details.

Choosing a format

Opinions on correct indentation, whitespace, and line lengths have always been controversial. Discussions on these topics are a distraction from creating and maintaining content.

At Posos, we use Prettier as a code formatter to keep the code style consistent (and to avoid off-topic discussions). You can read the Prettier documentation.

Prettier formats all the code and keeps the style consistent. Nevertheless, there are a few additional rules that you need to follow.

Using modern JavaScript features

You can use new features once every major browser — Chrome, Edge, Firefox, and Safari — supports them.

Spacing and indentation

Mark indentation with 2 spaces. Don't use the tab character. The end-of-line character is \n, the Unix convention.

To help you, here is a .editorconfig file with the recommended rules.

# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org

root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 90
trim_trailing_whitespace = true

Column limit

JavaScript code has a column limit of 90 characters. Except as noted below, any line that would exceed this limit must be line-wrapped.

Exceptions:

  1. ES module import and export from statements
  2. Lines where obeying the column limit is not possible or would hinder discoverability. Examples include:
    • A long URL which should be clickable in source.
    • A shell command intended to be copied-and-pasted.
    • A long string literal which may need to be copied or searched for wholly (e.g., a long file path).

Braces

Braces are used for all control structures

Braces are required for all control structures (i.e. if, else, for, do, while, as well as any others), even if the body contains only a single statement. The first statement of a non-empty block must begin on its own line.

invalid
if (shortCondition()) foo();

if (someVeryLongCondition())
doSomething();

for (let i = 0; i < foo.length; i++) bar(foo[i]);
valid
if (shortCondition()) {
foo();
}

if (someVeryLongCondition()) {
doSomething();
}

for (let i = 0; i < foo.length; i++) {
bar(foo[i]);
}

Nonempty blocks: K&R style

Braces follow the Kernighan and Ritchie style (Egyptian brackets) for nonempty blocks and block-like constructs:

  • No line break before the opening brace.
  • Line break after the opening brace.
  • Line break before the closing brace.
  • Line break after the closing brace if that brace terminates a statement or the body of a function or class statement, or a class method. Specifically, there is no line break after the brace if it is followed by else, catch, while, or a comma, semicolon, or right-parenthesis.

Example:

class InnerClass {
constructor() {}

method(foo) {
if (condition(foo)) {
try {
// Note: this might fail.
something();
} catch (err) {
recover();
}
}
}
}

Empty blocks: may be concise

An empty block or block-like construct may be closed immediately after it is opened, with no characters, space, or line break in between (i.e. {}), unless it is a part of a multi-block statement (one that directly contains multiple blocks: if/else or try/catch/finally).

invalid
if (condition) {
// …
} else if (otherCondition) {} else {
// …
}

try {
// …
} catch (e) {}
valid
function doNothing() {}

Statements

One statement per line

Each statement is followed by a line-break.

Semicolons are required

Every statement must be terminated with a semicolon.

Arrays

Use trailing commas

Include a trailing comma whenever there is a line break between the final element and the closing bracket.

Example:

const values = [
'first value',
'second value',
];

Do not use the variadic Array constructor

The constructor is error-prone if arguments are added or removed. Use a literal instead.

invalid
const a1 = new Array(x1, x2, x3);
const a2 = new Array(x1, x2);
const a3 = new Array(x1);
const a4 = new Array();

This works as expected except for the third case: if x1 is a whole number then a3 is an array of size x1 where all elements are undefined. If x1 is any other number, then an exception will be thrown, and if it is anything else then it will be a single-element array.

valid
const a1 = [x1, x2, x3];
const a2 = [x1, x2];
const a3 = [x1];
const a4 = [];

Non-numeric properties

Do not define or use non-numeric properties on an array (other than length). Use a Map (or Object) instead.

Item addition

When adding items to an array, use push() and not direct assignment. Consider the following array:

const pets = [];
invalid
pets[pets.length] = 'cat';
valid
pets.push('cat');

Destructuring

Array literals may be used on the left-hand side of an assignment to perform destructuring (such as when unpacking multiple values from a single array or iterable). A final rest element may be included (with no space between the ... and the variable name). Elements should be omitted if they are unused.

Example:

const [a, b, c, ...rest] = generateResults();

let [, b,, d] = someArray;

Destructuring may also be used for function parameters (note that a parameter name is required but ignored). Always specify [] as the default value if a destructured array parameter is optional, and provide default values on the left hand side.

invalid
function badDestructuring([a, b] = [4, 2]) {};
valid
function optionalDestructuring([a = 4, b = 2] = []) {};
note

For (un)packing multiple values into a function's parameter or return, prefer object destructuring to array destructuring when possible, as it allows naming the individual elements and specifying a different type for each.

Spread operator

Array literals may include the spread operator (...) to flatten elements out of one or more other iterables. The spread operator should be used instead of more awkward constructs with Array.prototype. There is no space after the ....

Example:

[...foo] // preferred over `Array.prototype.slice.call(foo)`
[...foo, ...bar] // preferred over `foo.concat(bar)`

Asynchronous methods

Writing asynchronous code improves performance and should be used when possible. In particular, you can use:

When both techniques are possible, we prefer using the simpler async/await syntax. Unfortunately, you can't use await at the top level unless you are in an ECMAScript module. CommonJS modules used by Node.js are not ES modules. If your example is intended to be used everywhere, avoid top-level await.

Classes

Class constructors

Constructors are optional. Subclass constructors must call super() before setting any fields or otherwise accessing this. Interfaces should declare non-method properties in the constructor.

Class names

When defining a class, use PascalCase (starting with a capital letter) for the class name and camelCase (starting with a lowercase letter) for the object property and method names.

invalid
class foo {
GetNext() {
return this.NextId++;
}
}
valid
class Foo {
getNext() {
return this.nextId++;
}
}

Getters and Setters

Do not use JavaScript getter and setter properties. They are potentially surprising and difficult to reason about, and have limited support in the compiler. Provide ordinary methods instead.

invalid
class Foo {
get next() {
return this.nextId++;
}
}
valid
class Color {
constructor(color) {
this.color = color;
}

setColor(color) {
this.color = color;
}

getColor() {
return this.color;
}
}

Comments

Block comment style

Block comments are indented at the same level as the surrounding code. They may be in /* … */ or //-style. For multi-line /* … */ comments, subsequent lines must start with * aligned with the * on the previous line, to make comments obvious with no extra context.

/*
* This is
* okay.
*/

// And so
// is this.

/* This is fine, too. */

Comments are not enclosed in boxes drawn with asterisks or other characters.

Do not use JSDoc (/** … */) for implementation comments.

Bad comments

Comments that explain "what is going on in the code" should be minimal. The code should be easy to understand without them.

A great rule about that is: "if the code is so unclear that it requires a comment, then maybe it should be rewritten instead".

Factor out functions

invalid
function showPrimes(n) {
nextPrime:
for (let i = 2; i < n; i++) {
// check if i is a prime number
for (let j = 2; j < i; j++) {
if (i % j == 0) {
continue nextPrime;
};
}
alert(i);
}
}
valid
function showPrimes(n) {
for (let i = 2; i < n; i++) {
if (!isPrime(i)) {
continue;
}
alert(i);
}
}
function isPrime(n) {
for (let i = 2; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}

The code is easily understandable and the function itself becomes the comment.

Create functions

invalid
// here we add whiskey
for(let i = 0; i < 10; i++) {
let drop = getWhiskey();
smell(drop);
add(drop, glass);
}
// here we add juice
for(let t = 0; t < 3; t++) {
let tomato = getTomato();
examine(tomato);
let juice = press(tomato);
add(juice, glass);
}
valid
function addWhiskey(container) {
for(let i = 0; i < 10; i++) {
let drop = getWhiskey();
// ...
}
}
function addJuice(container) {
for(let t = 0; t < 3; t++) {
let tomato = getTomato();
// ...
}
}
addWhiskey(glass);
addJuice(glass);

Functions themselves tell what is going on. There is nothing to comment. The code structure is also better when split as it is clear what every function does, what it takes and what it returns.

Good comments

Document function parameters and usage

Use JSDoc to document a function: usage, parameters, returned value.

Example:

/**
* Returns x raised to the n-th power.
*
* @param {number} x The number to raise.
* @param {number} n The power, must be a natural number.
* @return {number} x raised to the n-th power.
*/
function pow(x, n) {
// ...
}

Such comments allow us to understand the purpose of the function and use it the right way without looking in its code.

By the way, many editors can understand them as well and use them to provide autocomplete and some automatic code-checking.

Why is the task solved this way?

What is written is important. But what is not written may be even more important to understand what is going on. Why is the task solved exactly this way? The code gives no answer.

If there are many ways to solve the task, why this one? Especially when it is not the most obvious one.

Comments that explain the solution are very important. They help to continue development the right way.

Any subtle features of the code?

If the code has anything subtle and counter-intuitive, it should be commented.

Control structures

For loops

When loops are required, choose the appropriate one from for(;;), for...of, while, etc.

  • When iterating through all collection elements, avoid using the classical for (;;) loop; prefer for...of or forEach(). Note that if you are using a collection that is not an Array, you have to check that for...of is actually supported (it requires the variable to be iterable), or that the forEach() method is actually present.

    Do not use for (;;) — not only do you have to add an extra index, i, but you also have to track the length of the array. This can be error-prone for beginners.

    invalid
    const dogs = ['Rex', 'Lassie'];

    for (let i = 0; i < dogs.length; i++) {
    console.log(dogs[i]);
    }
    valid
    const dogs = ['Rex', 'Lassie'];

    for (const dog of dogs) {
    console.log(dog);
    }

    dogs.forEach((dog) => {
    console.log(dog);
    });
  • Make sure that you define the initializer properly by using the const keyword for for...of or let for the other loops. Don't omit it as it implicitly creates a global variable and will fail in strict mode.

    invalid
    const cats = ['Athena', 'Luna'];

    for (i of cats) {
    console.log(i);
    }
    valid
    const cats = ['Athena', 'Luna'];

    for (const cat of cats) {
    console.log(cat);
    }
  • When you need to access both the value and the index, you can use .forEach() instead of for (;;).

    invalid
    const gerbils = ['Zoé', 'Chloé'];

    for (let i = 0; i < gerbils.length; i++) {
    console.log(`Gerbil #${i}: ${gerbils[i]}`);
    }
    valid
    const gerbils = ['Zoé', 'Chloé'];

    gerbils.forEach((gerbil, index) => {
    console.log(`Gerbil #${index}: ${gerbil}`);
    });
caution

Never use for...in with arrays and strings.

info

Consider not using a for loop at all. If you are using an Array (or a String for some operations), consider using more semantic iteration methods instead, like map(), every(), findIndex(), find(), includes(), and many more.

Switch statements

Within a switch block, each statement group either terminates abruptly (with a break, return or thrown exception), or is marked with a comment to indicate that execution will or might continue into the next statement group. Any comment that communicates the idea of fall-through is sufficient (typically // fall through). This special comment is not required in the last statement group of the switch block.

Example:

switch (input) {
case 1:
case 2:
prepareOneOrTwo();
// fall through
case 3:
handleOneTwoOrThree();
break;
default:
handleLargeNumber(input);
}
  • Don't add a break statement after a return statement in a specific case.

    invalid
    switch (species) {
    case 'chicken':
    return farm.shed;
    break;
    case 'horse':
    return corral.entry;
    break;
    default:
    return '';
    }
    valid
    switch (species) {
    case 'chicken':
    return farm.shed;
    case 'horse':
    return corral.entry;
    default:
    return '';
    }
  • Use default as the last case, and don't end it with a break statement. If you need to do it differently, add a comment explaining why.

    invalid
    switch (species) {
    case 'chicken':
    return farm.shed;
    case 'horse':
    return corral.entry;
    }
    valid
    switch (species) {
    case 'chicken':
    return farm.shed;
    case 'horse':
    return corral.entry;
    default:
    return '';
    }
  • When declaring a local variable for a case, use braces to define a scope.

    invalid
    switch (fruits) {
    case 'Orange':
    const slice = fruit.slice();
    eat(slice);
    break;
    case 'Apple':
    const core = fruit.extractCore();
    recycle(core);
    break;
    }
    valid
    switch (fruits) {
    case 'Orange': {
    const slice = fruit.slice();
    eat(slice);
    break;
    }
    case 'Apple': {
    const core = fruit.extractCore();
    recycle(core);
    break;
    }
    }

Error handling

Exceptions are an important part of the language and should be used whenever exceptional cases occur. Always throw Errors or subclasses of Error: never throw string literals or other objects. Always use new when constructing an Error.

This treatment extends to Promise rejection values as Promise.reject(obj) is equivalent to throw obj; in async functions.

Custom exceptions provide a great way to convey additional error information from functions. They should be defined and used wherever the native Error type is insufficient.

Prefer throwing exceptions over ad-hoc error-handling approaches (such as passing an error container reference type, or returning an object with an error property).

It is very rarely correct to do nothing in response to a caught exception. When it truly is appropriate to take no action whatsoever in a catch block, the reason this is justified is explained in a comment.

invalid
try {
shouldFail();
fail('expected an error');
} catch (expected) {
}
valid
try {
return handleNumericResponse(response);
} catch {
// it's not numeric; that's fine, just continue
}

return handleTextResponse(response);
info

Keep in mind that only recoverable errors should be caught and handled. All non-recoverable errors should be let through and bubble up the call stack.

Functions

Function names

For function names, use camelCase, starting with a lowercase character. Use concise, human-readable, and semantic names where appropriate. The function names should say what they do.

invalid
function addToDate(date, month) {
// …
}

const date = new Date();

// It's hard to tell from the function name what is added
addToDate(date, 1);
valid
function addMonthToDate(month, date) {
// …
}

const date = new Date();
addMonthToDate(date, 1);

Function declarations

  • For top-level functions, use the function declaration over function expressions to define functions.

    invalid
    const sum = (a, b) => {
    return a + b;
    }
    valid
    function sum(a, b) {
    return a + b;
    }
  • When using anonymous functions as a callback (a function passed to another method invocation), if you do not need to access this, use an arrow function to make the code shorter and cleaner.

    invalid
    const arr = [1, 2, 3, 4];
    const sum = arr.reduce(function(a, b) {
    return a + b;
    });
    valid
    const arr = [1, 2, 3, 4];
    const sum = arr.reduce((a, b) => a + b);
  • Consider avoiding using arrow function to assign a function to an identifier. In particular, don't use arrow functions for methods. Use function declarations with the keyword function.

    invalid
    const x = () => {
    // …
    };
    valid
    function x() {
    // …
    }
  • When using arrow functions, use implicit return (also known as concise body) when possible.

    invalid
    arr.map((e) => {
    return e.id;
    });
    valid
    arr.map((e) => e.id);

Function default parameters

Optional parameters are permitted using the equals operator in the parameter list. Optional parameters must include spaces on both sides of the equals operator, be named exactly like required parameters, come after required parameters, and not use initializers that produce observable side effects. All optional parameters for concrete functions must have default values, even if that value is undefined. In contrast to concrete functions, abstract and interface methods must omit default parameter values.

Example:

function maybeDoSomething(required, optional = '', node = undefined) {
// …
}

Use default parameters sparingly. Prefer destructuring to create readable APIs when there are more than a small handful of optional parameters that do not have a natural order.

info

While arbitrary expressions including function calls may be used as initializers, these should be kept as simple as possible. Avoid initializers that expose shared mutable state, as that can easily introduce unintended coupling between function calls.

Function rest parameters

Use a rest parameter instead of accessing arguments. The rest parameter must be the last parameter in the list. There is no space between the ... and the parameter name. Never name a local variable or parameter arguments, which confusingly shadows the built-in name.

Example:

function variadic(array, ...numbers) {
// …
}

Objects

Object names

When defining an object instance, either a literal or via a constructor, use camelCase, starting with lower-case character, for the instance name.

invalid
const HanSolo = new person('Han Solo', 25);

const Luke = {
name: 'Luke Skywalker',
age: 25,
};
valid
const hanSolo = new Person('Han Solo', 25);

const luke = {
name: 'Luke Skywalker',
age: 25,
};

Object declarations

For creating general objects (i.e., when classes are not involved), use literals and not constructors.

invalid
const obj = new Object();
valid
const obj = {};

Object methods

To define methods, use the method definition syntax.

invalid
const obj = {
foo: function () {
// …
},
bar: function () {
// …
},
};
valid
const obj = {
foo() {
// …
},
bar() {
// …
},
};

Object properties

When possible, use the shorthand avoiding the duplication of the property identifier.

Include a trailing comma whenever there is a line break between the final property and the closing brace.

invalid
function createObject(name, age) {
return {name: name, age: age };
}

const luke = {
name: 'Luke Skywalker',
age: 25
};
valid
function createObject(name, age) {
return { name, age };
}

const luke = {
name: 'Luke Skywalker',
age: 25,
};
info

The Object.prototype.hasOwnProperty() method has been deprecated in favor of Object.hasOwn().

Object destructuring

Object destructuring patterns may be used on the left-hand side of an assignment to perform destructuring and unpack multiple values from a single object.

Destructured objects may also be used as function parameters, but should be kept as simple as possible: a single level of unquoted shorthand properties. Deeper levels of nesting and computed properties may not be used in parameter destructuring. Specify any default values in the left-hand-side of the destructured parameter ({str = 'some default'} = {}, rather than {str} = {str: 'some default'}), and if a destructured object is itself optional, it must default to {}.

invalid
function nestedTooDeeply({x: {num, str}}) {};

function nonShorthandProperty({num: a, str: b} = {}) {};

function computedKey({a, b, [a + b]: c}) {};

function nontrivialDefault({a, b} = {a: 2, b: 4}) {};
valid
function destructured(ordinary, {num, str = 'some default'} = {}) {};

Operators

This section lists our recommendations of which operators to use and when.

Conditional operators

When you want to store to a variable a literal value depending on a condition, use a conditional (ternary) operator instead of an if...else statement. This rule also applies when returning a value.

invalid
let x;
if (condition) {
x = 1;
} else {
x = 2;
}
valid
const x = condition ? 1 : 2;

Optional chaining operator

When you want to access nested object properties, use the optional chaining operator. It is a safe way to access nested object properties, even if an intermediate property doesn't exist.

invalid
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah'
},
};

const dogName = adventurer && adventurer.dog && adventurer.dog.name;
valid
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah'
},
};

const dogName = adventurer?.dog?.name;

When using optional chaining in contexts where the undefined value is not allowed can cause a TypeError or unexpected results.

invalid
const foo = {};

const { bar } = foo?.bar; // TypeError
valid
const foo = {};

const { bar } = foo?.bar || {};

Strict equality operator

Always use the strict equality (triple equals) and inequality operators over the loose equality (double equals) and inequality operators.

invalid
name == 'Shilpa';
age != 25;
valid
name === 'Shilpa';
age !== 25;

The only exception is for catching both null and undefined values.

Example:

if (someObjectOrPrimitive == null) {
// Checking for null catches both `null` and `undefined` for objects and
// primitives, but does not catch other falsy values like `0` or the empty
// string.
}

Shortcuts for boolean tests

Prefer shortcuts for boolean tests, unless different kinds of truthy or falsy values are handled differently.

invalid
if (x === false) {
// …
}
valid
if (!x) {
// …
}

Strings

String literals must be enclosed within single quotes by default, or within double quotes if the string contains a single quote.

invalid
const foo = "bar";
const example = "This \"example\" shouble be single quoted";
valid
const foo = 'bar';
const example = 'This "example" shouble be single quoted';
const hello = "Hello! I'm a bot!";
info

Double or single quotes? Prettier chooses the one which results in the fewest number of escapes. Check their documentation for more informations.

Template literals

Use template literals (delimited with `) over complex string concatenation, particularly if multiple string literals are involved. Template literals may span multiple lines.

invalid
const name = 'Shilpa';
console.log('Hi! My name is ' + name + '!');
valid
const name = 'Shilpa';
console.log(`Hi! My name is ${name}!`);

If a template literal spans multiple lines, it does not need to follow the indentation of the enclosing block, though it may if the added whitespace does not matter.

Example:

function arithmetic(a, b) {
return `Here is a table of arithmetic operations:
${a} + ${b} = ${a + b}
${a} - ${b} = ${a - b}
${a} * ${b} = ${a * b}
${a} / ${b} = ${a / b}`;
}

Line continuations

Do not use line continuations (that is, ending a line inside a string literal with a backslash) in either ordinary or template string literals. It can lead to tricky errors if any trailing whitespace comes after the slash, and is less obvious to readers.

invalid
const longString = 'This is a very long string that far exceeds the \
column limit. It unfortunately contains long stretches of spaces due \
to how the continued lines are indented.';
valid
const longString = 'This is a very long string that far exceeds the ' +
'column limit. It does not contain long stretches of spaces since ' +
'the concatenated strings are cleaner.';

Variables

Variable names

Good variable names are essential to understanding code.

  • Use short identifiers, and avoid non-common abbreviations. Good variable names are usually between 3 to 10-character long, but as a hint only.
  • Do not use the Hungarian notation naming convention. Do not prefix the variable name with its type.
  • For collections, avoid adding the type such as list, array, queue in the name. Use the content name in the plural form. There may be exceptions, like when you want to show the abstract form of a feature without the context of a particular application.
  • For primitive values, use camelCase, starting with a lowercase character. Do not use _. Use concise, human-readable, and semantic names where appropriate.
  • Avoid using articles and possessives. There may be exceptions, like when describing a feature in general without a practical context.
invalid
n                   // Meaningless.
nErr // Ambiguous abbreviation.
nCompConns // Ambiguous abbreviation.
wgcConnections // Only your group knows what this stands for.
pcReader // Lots of things can be abbreviated "pc".
cstmrId // Deletes internal letters.
kSecondsPerDay // Do not use Hungarian notation.
carArray // Do not add the type in the name.
currency_name // Not in camelCase
valid
errorCount          // No abbreviation.
dnsConnectionIndex // Most people know what "DNS" stands for.
referrerUrl // Ditto for "URL".
customerId // "Id" is both ubiquitous and unlikely to be misunderstood.
cars // Content name with plural form.

Variable declarations

When declaring variables and constants, use the let and const keywords, not var.

  • If a variable will not be reassigned, use const.

    invalid
    let name = 'Shilpa';
    console.log(name);
    valid
    const name = 'Shilpa';
    console.log(name);
  • If the value of a variable could change, use let.

    invalid
    const age = 40;
    age++; // TypeError
    console.log(age);
    valid
    let age = 40;
    age++;
    console.log(age);
  • Declare one variable per line.

    invalid
    let var1, var2;
    let var3 = var4 = 'Apapou'; // var4 is implicitly created as a global variable; fails in strict mode
    valid
    let var1;
    let var2;
    let var3 = 'Apapou';
    let var4 = var3;

Constants

Constant names use CONSTANT_CASE: all uppercase letters, with words separated by underscores.

invalid
// constants.js
export const nbPerPage = 30;
valid
// constants.js
export const NB_PER_PAGE = 30;

Enums

Enum names are written in UpperCamelCase, similar to classes, and should generally be singular nouns. Individual items within the enum are named in CONSTANT_CASE.

invalid
// enums.js
export const DIRECTION = {
North = 'north',
East = 'east',
West = 'west',
South = 'south',
};
valid
// enums.js
export const Direction = {
NORTH = 'north',
EAST = 'east',
WEST = 'west',
SOUTH = 'south',
};

Type coercion

Avoid implicit type coercions. In particular, avoid +val to force a value to a number and '' + val to force it to a string. Use Number() and String(), without new, instead.

invalid
class Person {
#name;
#birthYear;

constructor(name, year) {
this.#name = '' + name;
this.#birthYear = +year;
}
}
valid
class Person {
#name;
#birthYear;

constructor(name, year) {
this.#name = String(name);
this.#birthYear = Number(year);
}
}