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:
- ES module
importandexport fromstatements - 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.
if (shortCondition()) foo();
if (someVeryLongCondition())
doSomething();
for (let i = 0; i < foo.length; i++) bar(foo[i]);
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).
if (condition) {
// …
} else if (otherCondition) {} else {
// …
}
try {
// …
} catch (e) {}
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.
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.
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 = [];
pets[pets.length] = 'cat';
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.
function badDestructuring([a, b] = [4, 2]) { … };
function optionalDestructuring([a = 4, b = 2] = []) { … };
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.
class foo {
GetNext() {
return this.NextId++;
}
}
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.
class Foo {
get next() {
return this.nextId++;
}
}
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
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);
}
}
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
// 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);
}
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; preferfor...oforforEach(). Note that if you are using a collection that is not anArray, you have to check thatfor...ofis actually supported (it requires the variable to be iterable), or that theforEach()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.
invalidconst dogs = ['Rex', 'Lassie'];
for (let i = 0; i < dogs.length; i++) {
console.log(dogs[i]);
}
validconst 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
constkeyword forfor...oforletfor the other loops. Don't omit it as it implicitly creates a global variable and will fail in strict mode.
invalidconst cats = ['Athena', 'Luna'];
for (i of cats) {
console.log(i);
}
validconst 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 offor (;;).
invalidconst gerbils = ['Zoé', 'Chloé'];
for (let i = 0; i < gerbils.length; i++) {
console.log(`Gerbil #${i}: ${gerbils[i]}`);
}
validconst gerbils = ['Zoé', 'Chloé'];
gerbils.forEach((gerbil, index) => {
console.log(`Gerbil #${index}: ${gerbil}`);
});
Never use for...in with arrays and strings.
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
breakstatement after areturnstatement in a specific case.
invalidswitch (species) {
case 'chicken':
return farm.shed;
break;
case 'horse':
return corral.entry;
break;
default:
return '';
}
validswitch (species) {
case 'chicken':
return farm.shed;
case 'horse':
return corral.entry;
default:
return '';
}Use
defaultas the last case, and don't end it with abreakstatement. If you need to do it differently, add a comment explaining why.
invalidswitch (species) {
case 'chicken':
return farm.shed;
case 'horse':
return corral.entry;
}
validswitch (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.
invalidswitch (fruits) {
case 'Orange':
const slice = fruit.slice();
eat(slice);
break;
case 'Apple':
const core = fruit.extractCore();
recycle(core);
break;
}
validswitch (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.
try {
shouldFail();
fail('expected an error');
} catch (expected) {
}
try {
return handleNumericResponse(response);
} catch {
// it's not numeric; that's fine, just continue
}
return handleTextResponse(response);
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.
function addToDate(date, month) {
// …
}
const date = new Date();
// It's hard to tell from the function name what is added
addToDate(date, 1);
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.
invalidconst sum = (a, b) => {
return a + b;
}
validfunction 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.
invalidconst arr = [1, 2, 3, 4];
const sum = arr.reduce(function(a, b) {
return a + b;
});
validconst 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.
invalidconst x = () => {
// …
};
validfunction x() {
// …
}When using arrow functions, use implicit return (also known as concise body) when possible.
invalidarr.map((e) => {
return e.id;
});
validarr.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.
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.
const HanSolo = new person('Han Solo', 25);
const Luke = {
name: 'Luke Skywalker',
age: 25,
};
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.
const obj = new Object();
const obj = {};
Object methods
To define methods, use the method definition syntax.
const obj = {
foo: function () {
// …
},
bar: function () {
// …
},
};
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.
function createObject(name, age) {
return {name: name, age: age };
}
const luke = {
name: 'Luke Skywalker',
age: 25
};
function createObject(name, age) {
return { name, age };
}
const luke = {
name: 'Luke Skywalker',
age: 25,
};
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 {}.
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}) {};
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.
let x;
if (condition) {
x = 1;
} else {
x = 2;
}
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.
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah'
},
};
const dogName = adventurer && adventurer.dog && adventurer.dog.name;
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.
const foo = {};
const { bar } = foo?.bar; // TypeError
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.
name == 'Shilpa';
age != 25;
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.
if (x === false) {
// …
}
if (!x) {
// …
}
Strings
String literals must be enclosed within single quotes by default, or within double quotes if the string contains a single quote.
const foo = "bar";
const example = "This \"example\" shouble be single quoted";
const foo = 'bar';
const example = 'This "example" shouble be single quoted';
const hello = "Hello! I'm a bot!";
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.
const name = 'Shilpa';
console.log('Hi! My name is ' + name + '!');
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.
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.';
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.
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
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.
invalidlet name = 'Shilpa';
console.log(name);
validconst name = 'Shilpa';
console.log(name);If the value of a variable could change, use
let.
invalidconst age = 40;
age++; // TypeError
console.log(age);
validlet age = 40;
age++;
console.log(age);Declare one variable per line.
invalidlet var1, var2;
let var3 = var4 = 'Apapou'; // var4 is implicitly created as a global variable; fails in strict mode
validlet var1;
let var2;
let var3 = 'Apapou';
let var4 = var3;
Constants
Constant names use CONSTANT_CASE: all uppercase letters, with words separated by underscores.
// constants.js
export const nbPerPage = 30;
// 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.
// enums.js
export const DIRECTION = {
North = 'north',
East = 'east',
West = 'west',
South = 'south',
};
// 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.
class Person {
#name;
#birthYear;
constructor(name, year) {
this.#name = '' + name;
this.#birthYear = +year;
}
}
class Person {
#name;
#birthYear;
constructor(name, year) {
this.#name = String(name);
this.#birthYear = Number(year);
}
}