The Subtle Art of Adding Code Comments in JavaScript
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.
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, theparam
tag is used to describe a function parameter, while thereturns
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. @todo
tag 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 @throws
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`.
*
* @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. @example
tag is used to provide an example of how to use a function or object. Adding @example
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`.
*
* @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 @typedef
JSDoc 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 @typedef
can then be referenced with the @type
tag in other parts of the code.
In essence, @typedef
is used to define a custom type, while @type
is used to specify the type of an entity.
The @property
JSDoc tag is used to document properties of an object or class. It is typically used in conjunction with @typedef
to document the structure of an object or class. The @property
tag specifies the name and type of a property, as well as a description of its purpose.
Adding @type
, @typedef
, and @property
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.
*/
/**
* @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 @param
and @property
tags. The @property
tag is used to describe the properties or attributes of an object or class, whereas the @param
tag is used to describe the parameters of a function or method. Unlike the @property
tag, 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 @callback
tag 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 @callback
tag 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 @param
tag for the callback parameter of the factorialWithCallback
function.
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! 😊