The Subtle Art of Adding Code Comments in JavaScript

Frontendnachos
Engineered @ Publicis Sapient
10 min readMar 17, 2023

Debugging code without comments is like being a detective trying to solve a crime, but the only witness speaks in a foreign language, and there are some influential people (clients) breathing down your neck to solve the case yesterday.

developers debugging code

image source — Two Men Looking at a Laptop · Free Stock Photo (pexels.com)

Introduction

Adding code comments is important for several reasons:

1. Code readability. Comments help developers understand the purpose behind code by explaining it in plain language, making it easier to read and modify.

2. Maintenance. Comments help you remember the purpose of your code, making it easier to maintain and update in the future without accidentally causing issues.

3. Collaboration. Comments prevent errors and misinterpretations in collaborative projects by providing context for code changes, ensuring everyone is on the same page.

4. Documentation. Code comments provide clarity on purpose and functionality, ensuring compliance with industry standards and best practices. They make code more readable, maintainable, and easier to work with, just like LEGO instructions guide in building a set.

By taking the time to add meaningful and accurate comments to code, developers can improve the overall quality and maintainability of their projects.

Mental Model for Adding Comments to Code

Before writing comments around a piece of code, it is important to be clear about the intent of doing so. A code comment is a communication mechanism that should communicate the following aspects of the code:

1. Purpose. Explain the purpose of the code, what problem it solves and why it exists.

2. Input. Specify the inputs required by the code, including their types, expected values and whether they are optional or mandatory.

3. Output. Specify the output produced by the code, including its type and any relevant details.

4. Usage. Provide examples of how to use the code, including relevant code snippets.

5. Linking. Add links to related code and external resources.

6. Best practices. Highlight any best practices or gotchas related to the code.

By following the right mental model, developers can ensure that their code is well-documented, easy to understand, and maintainable over time.

Consistency and Standardization

It is crucial to maintain consistency in the formatting and the style of comments throughout the codebase. Using a tool such as JS Doc (JavaScript documentation), which is a set of documentation standards and tools for documenting JavaScript code, is a good option.

The following are some of JS Doc’s features:

  • It uses tags to capture information about the code’s structure, function, parameters, return values, and other details to create a human-readable reference for other developers.
  • A JS Doc tag starts with an @symbol, followed by the tag name and any relevant information. For example, the param tag is used to describe a function parameter, while the returns tag is used to describe the return value of a function.
  • It can generate documentation in HTML or other formats from inline comments in the source code.

Let’s look at some of the most commonly-used JS Doc tags in detail. For reference, we will be adding code comments to the following JS Code:

function factorial(n) {
if (typeof n !== "number") {
throw new TypeError("Input must be a number");
}
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}

function factorialWithCallback(n, callback) {
try {
const result = {
input: n,
output: factorial(n),
};
callback(null, result);
} catch (error) {
callback(error);
}
}

Do not worry if it is unclear what the code accomplishes. The next sections will provide an explanation.

Commonly-used JS Doc Tags

Let’s have a look at a couple of JS Doc tags that are used quite frequently.

  • @todo

Before doing something, it is important to acknowledge that it needs to be done, and that it brings value to the system. @todotag does exactly the same.

/**
* @todo Add details about the factorial calculation and edge cases.
*/
function factorial(n) {
if (typeof n !== "number") {
throw new TypeError("Input must be a number");
}
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}

/**
* @todo Add an example usage of the callback function.
*/
function factorialWithCallback(n, callback) {
try {
const result = {
input: n,
output: factorial(n),
};
callback(null, result);
} catch (error) {
callback(error);
}
}
  • @description

The first step is to describe what the code does. @description tag is used to provide a detailed description of a function, object, or property. Let’s see how the code looks after adding this tag.

/**
* @description A function that calculates the factorial of a number using recursion.
*/
function factorial(n) {
if (typeof n !== "number") {
throw new TypeError("Input must be a number");
}
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}

/**
* @description A version of the factorial function that returns the result via a callback.
*/
function factorialWithCallback(n, callback) {
try {
const result = {
input: n,
output: factorial(n),
};
callback(null, result);
} catch (error) {
callback(error);
}
}
  • @param

Here comes the part where it is told what input does the code take. @param tag is used to describe the parameters of a function. Adding it to the example makes the code look like this:

/**
* @description A function that calculates the factorial of a number using recursion.
*
* @param {number} n - The number for which to calculate the factorial.
*/
function factorial(n) {
if (typeof n !== "number") {
throw new TypeError("Input must be a number");
}
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}

/**
* @description A version of the factorial function that returns the result via a callback.
*
* @param {number} n - The number for which to calculate the factorial.
* @param {function} callback - The function to call with the result of the calculation.
*/
function factorialWithCallback(n, callback) {
try {
const result = {
input: n,
output: factorial(n),
};
callback(null, result);
} catch (error) {
callback(error);
}
}
  • @returns

So far we know what the function does, and the inputs it takes. Next comes describing the return value of a function. Adding @returns to the example makes the code look like this:

/**
* @description A function that calculates the factorial of a number using recursion.
*
* @param {number} n - The number for which to calculate the factorial.
*
* @returns {number} The factorial of `n`.
*/
function factorial(n) {
if (typeof n !== "number") {
throw new TypeError("Input must be a number");
}
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}

/**
* @description A version of the factorial function that returns the result via a callback.
*
* @param {number} n - The number for which to calculate the factorial.
* @param {function} callback - The function to call with the result of the calculation.
*
* @returns {void}
*/
function factorialWithCallback(n, callback) {
try {
const result = {
input: n,
output: factorial(n),
};
callback(null, result);
} catch (error) {
callback(error);
}
}
  • @throws

There will be times when the function will encounter errors and will not be able to produce the desired results. @throws helps in documenting those scenarios. Adding @throwsto the example makes the code look like this:

/**
* @description A function that calculates the factorial of a number using recursion.
*
* @param {number} n - The number for which to calculate the factorial.
*
* @returns {number} The factorial of `n`.
*
* @throws {TypeError} If the input is not a number.
*/
function factorial(n) {
if (typeof n !== "number") {
throw new TypeError("Input must be a number");
}
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}

/**
* @description A version of the factorial function that returns the result via a callback.
*
* @param {number} n - The number for which to calculate the factorial.
* @param {function} callback - The function to call with the result of the calculation.
*
* @returns {void}
*/
function factorialWithCallback(n, callback) {
try {
const result = {
input: n,
output: factorial(n),
};
callback(null, result);
} catch (error) {
callback(error);
}
}
  • @example

At times, the consumer or invoker of the code is not interested in the low-level implementation details, and is concerned about the invocation and getting the expected results. @exampletag is used to provide an example of how to use a function or object. Adding @exampleto the example makes the code look like this:

/**
* @description A function that calculates the factorial of a number using recursion.
*
* @param {number} n - The number for which to calculate the factorial.
*
* @returns {number} The factorial of `n`.
*
* @throws {TypeError} If the input is not a number.
*
* @example
* factorial(5);
* // returns 120
*/
function factorial(n) {
if (typeof n !== "number") {
throw new TypeError("Input must be a number");
}
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}

/**
* @description A version of the factorial function that returns the result via a callback.
*
* @param {number} n - The number for which to calculate the factorial.
* @param {function} callback - The function to call with the result of the calculation.
*
* @returns {void}
*
* @example
* factorialWithCallback(5, (error, result) => {
* if (error) {
* console.error(error);
* } else {
* console.log(result);
* }
* });
* // logs { input: 5, output: 120 }
*/
function factorialWithCallback(n, callback) {
try {
const result = {
input: n,
output: factorial(n),
};
callback(null, result);
} catch (error) {
callback(error);
}
}
  • @type , @typedef , and @property

The @type JSDoc tag is used to specify the type of a variable, function, or expression. It provides additional information about the type of the entity being documented and helps with code readability and maintainability.

The @typedefJSDoc tag, on the other hand, is used to define a custom type. This can be useful in complex projects where you have custom data structures or types that need to be documented. The custom type defined with @typedefcan then be referenced with the @typetag in other parts of the code.

In essence, @typedefis used to define a custom type, while @typeis used to specify the type of an entity.

The @propertyJSDoc tag is used to document properties of an object or class. It is typically used in conjunction with @typedefto document the structure of an object or class. The @propertytag specifies the name and type of a property, as well as a description of its purpose.

Adding @type, @typedef, and @propertyto the example makes the code look like this:

/**
* @typedef {Object} FactorialResult
*
* @property {number} input - The input to the factorial function.
* @property {number} output - The output of the factorial function.
*/

/**
* @description A function that calculates the factorial of a number using recursion.
*
* @param {number} n - The number for which to calculate the factorial.
*
* @returns {number} The factorial of `n`.
*
* @throws {TypeError} If the input is not a number.
*
* @example
* factorial(5);
* // returns 120
*/
function factorial(n) {
if (typeof n !== "number") {
throw new TypeError("Input must be a number");
}
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}

/**
* @description A version of the factorial function that returns the result via a callback.
*
* @param {number} n - The number for which to calculate the factorial.
* @param {function} callback - The function to call with the result of the calculation.
*
* @returns {void}
*
* @example
* factorialWithCallback(5, (error, result) => {
* if (error) {
* console.error(error);
* } else {
* console.log(result);
* }
* });
* // logs { input: 5, output: 120 }
*/
function factorialWithCallback(n, callback) {
/**
* @type {FactorialResult}
*/
const result = {
input: n,
output: factorial(n),
};
try {
callback(null, result);
} catch (error) {
callback(error);
}
}

It is important to note the difference between @paramand @propertytags. The @propertytag is used to describe the properties or attributes of an object or class, whereas the @paramtag is used to describe the parameters of a function or method. Unlike the @propertytag, the @param tag is used within the JSDocs block of a function or method, not outside it.

  • @callback

It is used to describe a callback function. The @callbacktag specifies the parameters of the callback function and their types. Adding @callback tag to the example makes the code look like this:

/**
* @typedef {Object} FactorialResult
*
* @property {number} input - The input to the factorial function.
* @property {number} output - The output of the factorial function.
*/

/**
* @callback FactorialCallback
*
* @param {Error} error - The error that occurred during the calculation, if any.
* @param {FactorialResult} result - The result of the calculation.
*/

/**
* @description A function that calculates the factorial of a number using recursion.
*
* @param {number} n - The number for which to calculate the factorial.
*
* @returns {number} The factorial of `n`.
*
* @throws {TypeError} If the input is not a number.
*
* @example
* factorial(5);
* // returns 120
*/
function factorial(n) {
if (typeof n !== "number") {
throw new TypeError("Input must be a number");
}
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}

/**
* @description A version of the factorial function that returns the result via a callback.
*
* @param {number} n - The number for which to calculate the factorial.
* @param {FactorialCallback} callback - The function to call with the result of the calculation.
*
* @returns {void}
*
* @example
* factorialWithCallback(5, (error, result) => {
* if (error) {
* console.error(error);
* } else {
* console.log(result);
* }
* });
* // logs { input: 5, output: 120 }
*/
function factorialWithCallback(n, callback) {
/**
* @type {FactorialResult}
*/
const result = {
input: n,
output: factorial(n),
};
try {
callback(null, result);
} catch (error) {
callback(error);
}
}

In this code, the @callbacktag is used to define a custom type FactorialCallback, which is a callback function that is passed as a parameter to the factorialCallback function.

The FactorialCallback type is then referenced in the @paramtag for the callback parameter of the factorialWithCallbackfunction.

Example/Demo

A preview of the above snippet and the documentation that it generates using JS Doc can be checked here.

Kindly note that the example is a very simplified implementation to show the reader the potential that may be unlocked once all of these techniques are understood and implemented in their projects. 🙂

Summary

In a world full of complex and intricate codebases, code comments are a friend we all need. With benefits ranging from improved code readability to automatically generated documentation, tools like JS Doc are essential to any development toolkit.

Whether you’re a seasoned developer or just starting out, taking the time to document your code in a standardised way will not only benefit the product in the short-term, but also ensure the longevity and maintainability of the codebase for years to come.

I’d encourage you to use these systems into your projects. Trust me, your future self will thank you for it! 😊

--

--