JavaScript
Article

15 Ways to Write Self-documenting JavaScript

By Jani Hartikainen

This article was peer reviewed by Tim Severien and Mark Brown. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Isn’t it fun to find a comment in code that’s completely out of place and useless?

It’s an easy mistake to make: you change some code, and forget to remove or update the comment. A bad comment won’t break your code, but imagine what would happen when debugging. You read the comment. It says one thing, while the code does another. You’ll probably end up wasting time figuring it out, and in the worst case, it might even mislead you!

But writing code with zero comments isn’t an option. In my more than 15 years of programming experience, I’ve never seen a codebase where comments were completely unnecessary.

However, there are ways to reduce the need for comments. We can make use of certain coding techniques to clarify our code, simply by using the programming language’s features to our advantage.

Not only does this help make our code easier to understand, it can also help improve the design of the program overall!

This type of code is often called self documenting. Let me show you how you can take this approach to coding right now. While the examples I’ll present here are in JavaScript, you can apply most of the techniques in other languages as well.

Overview of Techniques

Some programmers include comments as part of self-documenting code. In this article, we’ll only focus on code. Comments are important, but they’re a large topic to be covered separately.

We can split the techniques for self-documenting code into three broad categories:

  • structural, where the structure of code or directories is used to clarify the purpose
  • naming related, such as function or variable naming
  • syntax related, where we make use of (or avoid using) features of the language to clarify code.

Many of these are simple on paper. The challenge comes from knowing when to use what technique. I’ll show you some practical examples as we tackle each one.

Structural

First, let’s look at the structural category. Structural changes refer to shifting code around for enhanced clarity.

Move code into a function

This is the same as the “extract function” refactoring — meaning that we take existing code and move it into a new function: we “extract” the code out into a new function.

For example, try to guess what the following line does:

var width = (value - 0.5) * 16;

Not very clear; a comment here could be quite useful. Or, we could extract a function to make it self documenting:

var width = emToPixels(value);

function emToPixels(ems) {
    return (ems - 0.5) * 16;
}

The only change was I moved the calculation into a function. The function’s name is descriptive of what it does, so the code no longer needs clarification. As an additional benefit, we now have a useful helper function that you can use elsewhere, so this method also helps reduce duplication.

Replace conditional expression with function

If clauses with multiple operands can often be hard to understand without a comment. We can apply a similar method as above to clarify them:

if(!el.offsetWidth || !el.offsetHeight) {
}

What is the purpose of the above condition?

function isVisible(el) {
    return el.offsetWidth && el.offsetHeight;
}

if(!isVisible(el)) {
}

Again, we moved the code into a function and the code is immediately much easier to understand.

Replace expression with variable

Replacing something with a variable is similar to moving code into a function, but instead of a function, we simply use a variable.

Let’s take a look at the example with if clauses again:

if(!el.offsetWidth || !el.offsetHeight) {
}

Instead of extracting a function, we can also clarify this by introducing a variable:

var isVisible = el.offsetWidth && el.offsetHeight;
if(!isVisible) {
}

This can be a better choice than extracting a function — for example, when the logic you want to clarify is very specific to a certain algorithm used only in one place.

The most common use for this method is mathematical expressions:

return a * b + (c / d);

We can clarify the above by splitting the calculation:

var multiplier = a * b;
var divisor = c / d;
return multiplier + divisor;

Because I’m terrible at math, imagine the above example has some meaningful algorithm. In any case, the point is that you can move complex expressions into variables that add meaning to otherwise hard-to-understand code.

Class and module interfaces

The interface — that is, the public methods and properties — of a class or module can act as documentation on its usage.

Let’s look at an example:

class Box {
    setState(state) {
        this.state = state;
    }

    getState() {
        return this.state;
    }
}

This class could contain some other code in it as well. I purposely kept the example simple, to illustrate how the public interface is documentation

Can you tell how this class should be used? Maybe with a little bit of work, but it isn’t very obvious.

Both of the functions have reasonable names: what they do is clear from their name. But despite this, it’s not very clear how you should be using them. Most likely you would need to read more code or the documentation for the class to figure it out.

What if we changed it to something like this:

class Box {
    open() {
        this.state = 'open';
    }

    close() {
        this.state = 'closed';
    }

    isOpen() {
        return this.state === 'open';
    }
}

Much easier to see the usage, don’t you think? Notice that we only changed the public interface; the internal representation is still the same with the this.state property.

Now you can tell at a glance how the Box class is used. This shows that even though the first version had good names in the functions, the complete package was still confusing, and how, with simple decisions like this, you can have a very big impact. You always have to think of the big picture.

Code grouping

Grouping different parts of code can also act as a form of documentation.

For example, you should always aim to declare your variables as close to where they are being used as possible, and try to group variable uses together.

This can be used to indicate a relationship between the different parts of the code, so that anyone changing it in the future has an easier time finding which parts they may also need to touch.

Consider the following example:

var foo = 1;

blah()
xyz();

bar(foo);
baz(1337);
quux(foo);

Can you see at a glance how many times foo was used? Compare it to this:

var foo = 1;
bar(foo);
quux(foo);

blah()
xyz();

baz(1337);

With all the uses of foo grouped together, we can easily see which parts of the code depend on it.

Use pure functions

Pure functions are much easier to understand than functions that rely on state.

What is a pure function? When calling a function with the same parameters, if it always produces the same output, it’s most likely a so-called “pure” function. This means the function should not have any side effects or rely on state — such as time, object properties, Ajax, etc.

These types of functions are easier to understand, as any values affecting their output are passed in explicitly. You won’t have to dig around to figure out where something comes from, or what affects the result, as it’s all in plain sight.

Another reason these types of functions make for more self-documenting code is you can trust their output. No matter what, the function will always return output only based on what parameters you give it. It also won’t affect anything external, so you can trust it won’t cause an unexpected side effect.

A good example of where this goes wrong is document.write(). Experienced JS developers know you shouldn’t use it, but many beginners stumble with it. Sometimes it works well — but other times, in certain circumstances, it can wipe the whole page clean. Talk about a side effect!

For a better overview of what a pure function is, see the article Functional Programming: Pure Functions.

Directory and file structure

When naming files or directories, follow the same naming convention as used in the project. If there’s no clear convention in the project, follow the standard for your language of choice.

For example, if you’re adding new UI-related code, find where similar functionality is in the project. If UI-related code is placed in src/ui/, you should do the same.

This makes it easier to find the code and shows its purpose, based on what you already know about the other pieces of code in the project. All UI code is in the same place, after all, so it must be UI related.

Naming

There’s a popular quote about the two hard things in computer science:

There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton

So let’s take a look at how we can use naming things to make our code self documenting.

Rename function

Function naming is often not too difficult, but there’s some simple rules that you can follow:

  • Avoid using vague words like “handle” or “manage”: handleLinks(), manageObjects(). What do either of these do?
  • Use active verbs: cutGrass(), sendFile() — functions that actively perform something.
  • Indicate return value: getMagicBullet(), readFile(). This is not something you can always do, but it’s helpful where it makes sense.
  • Languages with strong typing can use type signatures to help indicate return values as well.

Rename variable

With variables, here are two good rules of thumb:

  • Indicate units: if you have numeric parameters, you can include the expected unit. For example, widthPx instead of width to indicate the value is in pixels instead of some other unit.
  • Don’t use shortcuts: a or b are not acceptable names, except for counters in loops.

Follow established naming conventions

Try to follow the same naming conventions in your code. For example, if you have an object of a specific type, call it the same name:

var element = getElement();

Don’t suddenly decide to call it a node:

var node = getElement();

If you follow the same conventions as elsewhere in the codebase, anyone reading it can make safe assumptions about the meanings of things based on what it means elsewhere.

Use meaningful errors

Undefined is not an object!

Everyone’s favorite. Let’s not follow JavaScript’s example, and let’s make sure any errors our code throws have a meaningful message in them.

What makes an error message meaningful?

  • it should describe what the problem was
  • if possible, it should include any variable values or other data which caused the error
  • key point: the error should help us find out what went wrong — therefore acting as documentation on how the function should work.

Syntax

Syntax-related methods for self-documenting code can be a little bit more language specific. For example, Ruby and Perl allow you to do all kinds of strange syntax tricks, which, in general, should be avoided.

Let’s take a look at a few that happen with JavaScript.

Don’t use syntax tricks

Don’t use strange tricks. Here’s a good way to confuse people:

imTricky && doMagic();

It’s equivalent to this much more sane looking code:

if(imTricky) {
    doMagic();
}

Always prefer the latter form. Syntax tricks are not going to do anyone any favors.

Use named constants, avoid magic values

If you have special values in your code — such as numbers or string values — consider using a constant instead. Even if it seems clear now, more often than not, when coming back to it in a month or two, nobody will have any idea why that particular number was put there.

const MEANING_OF_LIFE = 42;

(If you’re not using ES6, you can use a var and it’ll work equally well.)

Avoid boolean flags

Boolean flags can make for hard-to-understand code. Consider this:

myThing.setData({ x: 1 }, true);

What is the meaning of true? You have absolutely no idea, unless you dig into the source for setData() and find out.

Instead, you can add another function, or rename an existing function:

myThing.mergeData({ x: 1 });

Now you can immediately tell what’s going on.

Use language features to your advantage

We can even use some features of our chosen language to better communicate the intention behind some code.

A good example of this in JavaScript are the array iteration methods:

var ids = [];
for(var i = 0; i < things.length; i++) {
  ids.push(things[i].id);
}

The above code collects a list of IDs into a new array. However, in order to know that, we need to read the whole body of the loop. Compare it with using map():

var ids = things.map(function(thing) {
  return thing.id;
});

In this case, we immediately know that this produces a new array of something, because that’s the purpose of map(). This can be beneficial especially if you have more complicated looping logic. There’s a list of other iteration functions on MDN.

Another example with JavaScript is the const keyword.

Often, you declare variables where the value is supposed to never change. A very common example is when loading modules with CommonJS:

var async = require('async');

We can make the intention of never changing this even more clear:

const async = require('async');

As an added benefit, if someone ever accidentally tries to change this, we’ll now get an error.

Anti-patterns

With all these methods at your disposal, you can do a lot of good. However, there are some things you should be careful about …

Extracting for the sake of having short functions

Some people advocate the use of tiny tiny functions, and if you extract everything out, that’s what you can get. However, this can detrimentally affect how easy the code is to understand.

For example, imagine you’re debugging some code. You look in function a(). Then, you find it uses b(), which then uses c(). And so on.

While short functions can be great and easy to understand, if you’re only using the function in a single place, consider using the “replace expression with variable” method instead.

Don’t force things

As usual, there’s no absolute right way to do this. Therefore, if something doesn’t seem like it’s a good idea, don’t try to force it.

Conclusion

Making your code self documenting goes a long way to improving the maintainability of your code. Every comment is additional cruft that has to be maintained, so eliminating comments where possible is a good thing.

However, self-documenting code doesn’t replace documentation or comments. For example, code is limited in expressing intent, so you need to have good comments as well. API documentation is also very important for libraries, as having to read the code is not feasible unless your library is very small.

  • http://simplestepscode.com/ Yaphi Berhanu

    I love this article! I’m definitely guilty of using vague verbs like “handle” in my functions, and there a bunch of other points I’m going to start noticing now.

  • Lê Hưng

    Thanks for your article. it’s useful and I always applied it.

  • http://www.lazywarrior.com/ Patrick

    Excellent collection of tips! Nicely put together.

    While I would argue that “handleXXX”-type function names are sometimes the best names you give a function whose code does more than one thing (e.g. Transform AND Store AND log…), it’s certainly good to remember that, as soon as you can narrow down the purpose of a function, you should indeed name it more explicitly.

    • http://codeutopia.net/ Jani Hartikainen

      Yeah, sometimes it can be difficult to come up with good names. It can be a sign though: If you can’t think of a good name for a function, it could be doing too much work, or work that should be split between different functions :)

      • http://www.lazywarrior.com/ Patrick

        Yes true. But in those cases, even if you split the functionality of the handler out into other functions, you still need to pass in just one main handler function which then still has the same problem.

        • http://codeutopia.net/ Jani Hartikainen

          In my experience those kinds of functions often do some kind of a process.. like, say, create a user, assign it into a group and add a log entry. Especially if you do split it up, it can help clarify what that process is, which can help you find a better name :) But you’re right, sometimes it can still be hard to come up with a more descriptive name.

  • http://www.lazywarrior.com/ Patrick

    Whoa… I assume you’re being sarcastic?

    • http://webuniverse.io Serge Zarouski

      No sir. People ignore comments, unless they get frustrated with the code. Same is true when they edit code, so comments can easily get disconnected with the original code and even become misleading. On the other side it is hard to ignore code. Note that I don’t say that every single place should have crazy long variable/function names, only edge cases like in the example above.

      • http://www.lazywarrior.com/ Patrick

        I think people will rather read a comment above the variable declaration than read (let alone use) a variable name that almost breaks any linter’s long line limit all on its own.

        Just imagine the potential for typos! And it’s not hard to see that a variable like this is vastly less readable than any comment you could write in English above.

        • http://webuniverse.io Serge Zarouski

          I think you’re missing the point. I work with people that make that kind of mistakes, like I mentioned above. As for linter validation, I find this point week as you can come up with a shorter version, I just tried to be expressive for the sake of the example. Same for typos, unless you use notepad (in 2016) editors typically provide autocompletion, at least for local variables. So far I didn’t see people struggling a lot while reading camel case names, javascript programmers are used to that.

          • http://www.lazywarrior.com/ Patrick

            I wasn’t complaining about camelCase, everybody uses it and should, and you’re right about editor support to avoid typos.

            It’s just that your example is so excessive as to be ridiculous, which is why I asked if you were being sarcastic.
            If I saw code with a long variable or function name like that, I’d want to have a stern talk with that developer.

          • http://webuniverse.io Serge Zarouski

            I can see that :) I see that you don’t like side effect of the solution and I respect that. Yet I need an alternative to comments for the reasons mentioned above and I don’t see another solution.

          • http://codeutopia.net/ Jani Hartikainen

            I agree that the name at least in the example is a bit long. I can see where you’re coming from though, and unless you want to add code reviews to the process, the other alternative would be to make use of code structure. If looking at your example, we could say have `function isOperaMini`, which tells a part of the story. Then, the variable name within this function can be much shorter, because the context is more clear.

          • http://webuniverse.io Serge Zarouski

            Yes, I was considering adding that to the original comment at the beginning to follow example from your article, but hesitated at the end :)

          • http://webuniverse.io Serge Zarouski

            Here is why I hesitated, I chose to be explicit: https://www.nczonline.net/newsletter/archive/d863bf3ebb/.

  • Semen

    Very good article! Thx you.

  • Watan Deep

    The only one I disagree with declaring variables close to where they’re being used. I personally like to declare all variables at the top so I can immediately tell which variables are used in the function. However, that’s more of a personal preference. A very good article overall! Lots of good points in there.

  • http://mg.to/ Michael Geary

    That quote about the hard things in computer science is the boring version. Here’s the fun version:

    “There are two hard things in computer science: naming things, cache invalidation, and off-by-one errors.”

  • https://franciskim.co Francis Kim

    Great points!

  • hgoebl

    This is all true – very good article.
    With the example `imTricky && doMagic();` I’m with you as well. But before writing endless if-statements or ternary operators, I often use something like `let name = obj && obj.address && obj.address.name`. In other words I’m missing an operator like `let name = obj.?address.?name`.
    What do you think? Too tricky?

    • http://codeutopia.net/ Jani Hartikainen

      I think something like that can be OK if the use is clear. However, it does raise the question of why do you need to do that? Usually your objects should be reasonably well-formed – why would an object suddenly not have the property? (assuming we’re not talking of some kind of validation code, where I can certainly see it being a possibility).

      Not saying it’s automatically wrong, but just something to think about :)

      • hgoebl

        There are cases where objects have optional properties, e.g. `businessAddress` for contacts. The example is a bit derived, but generally I often ask myself how to do a readable and almost-clean substitute for the missing “Existential Operator”, see https://esdiscuss.org/topic/the-existential-operator

  • Gevorg Harutyunyan

    Great article, thanks!

  • jguang zhang

    good

  • Osvaldo Maria

    Grear post.

  • Irvandoval

    Thank you, very helpful tips!

  • http://methodicalprogrammer.com jeshan

    great article. I think that “Code grouping” and “Use pure functions” should be adopted by more people. They work together and give high returns.

  • ossan-engineer

    Great post! I’d like to translate this post and share with Japanese developers. Can I do that?

    • James Hibbard

      Hi there,

      This should be fine, provided the site isn’t a for-profit venture, and that you credit and link back to the original SitePoint post.

      Translation requests need to be made by email to the Head of Content, currently Ophelie Lechat (mail)

      • ossan-engineer

        Thank you for your advice. I’ll contact with her.

  • Thiago Santos

    Thank you so much!!! This is what I was looking for a long time.

  • Rohit

    like the replacing expression with variable idea. I find 2 other areas in javascript hard to read
    1. Chaining of functions(promises)
    2. Anonymous functions in call backs and also in modules.

    • Andreas Bernhard

      Regarding (2)
      Nice way to treat anonymous functions is to give them actually a name, e.g.

      ´´´
      function functionWithCallbacks(successCb, errorCb){

      }
      ´´´

      When calling the function you can either directly name them …

      ´´´
      functionWithCallbacks(
      function success(){

      },
      function error(){

      }
      )
      ´´´

      Or reference them via a variable and then use them (necessary especially when working with event listeners as you are otherwise not able unassign them later)

      ´´´
      var onKeyDownFn = function(ev){ … };

      window.addEventListener(“keydown”, onKeyDownFn)

      window.removeEventListener(“keydown”, onKeyDownFn)
      ´´´

      • http://codeutopia.net/ Jani Hartikainen

        As an added bonus of this approach, the function name will show up in stack traces which can make debugging code easier :)

  • Ye Qingnan

    This article is cool, thanks : ) it helps a lot ~

  • https://monda.hu László Monda

    As an advocate and practicioner of clean code I love your article! I wish every developer followed them.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.