JavaScript
Article

The Importance of Writing Code That Humans Can Read

By Tim Severien

This article was peer reviewed by Matt Burnett, Simon Codrington and Nilson Jacques. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Have you ever finished a project in a single run without ever needing to look at the code again? Neither have I. When working on an older project, you probably want to spend little to no time figuring out how code works. Readable code is imperative to keep a product maintainable and to keep yourself, and your colleagues or collaborators happy.

Exaggerated examples of unreadable code can be found on JS1k contests, where the goal is to write the best JavaScript applications with 1024 characters or less, and JSF*ck (NSFW, by the way), an esoteric programming style which uses only six different characters to write JavaScript code. Looking at code on either of these sites will make you wonder what is going on. Imagine writing such code and trying to fix a bug months later.

If you surf the internet regularly, or build interfaces, you may know that it’s easier to quit a large, bulky form than one that seems simple and small. The same can be said about code. When perceived as easier to read and to work on, one may enjoy working on it more. At least it will save you tossing out your computer in frustration.

In this article, I’m going to look at tips and tricks to make your code more readable, as well as pitfalls to avoid.

Code Splitting

Sticking with the form analogy, forms are sometimes split in parts, making them appear less of a hurdle. The same can be done with code. By splitting it up into parts, readers can skip to what is relevant for them instead of ploughing through a jungle.

Across Files

For years, we have been optimising things for the web. JavaScript files are no exception of that. Think of minification and pre-HTTP/2, we saved HTTP requests by combining scripts into a single one. Today, we can work as we want and have a task runner like Gulp or Grunt process our files. It’s safe to say we get to program the way we like, and leave optimization (such as concatenation) to tools.

// Load user data from API
var getUsersRequest = new XMLHttpRequest();
getUsersRequest.open('GET', '/api/users', true);
getUsersRequest.addEventListener('load', function() {
    // Do something with users
});

getUsersRequest.send();

//---------------------------------------------------
// Different functionality starts here. Perhaps
// this is an opportunity to split into files.
//---------------------------------------------------

// Load post data from API
var getPostsRequest = new XMLHttpRequest();
getPostsRequest.open('GET', '/api/posts', true);
getPostsRequest.addEventListener('load', function() {
    // Do something with posts
});

getPostsRequest.send();

Functions

Functions allow us to create blocks of code we can reuse. Normally, a function’s content is indented, making it easy to see where a function starts and ends. A good habit is to keep functions tiny—10 lines or less. When a function is named correctly, it’s also easy to understand what is happening when it’s being called. We’ll get to naming conventions later.

// Load user data from API
function getUsers(callback) {
    var getUsersRequest = new XMLHttpRequest();
    getUsersRequest.open('GET', '/api/users', true);
    getUsersRequest.addEventListener('load', function() {
        callback(JSON.parse(getUsersRequest.responseText));
    });

    getUsersRequest.send();
}

// Load post data from API
function getPosts(callback) {
    var getPostsRequest = new XMLHttpRequest();
    getPostsRequest.open('GET', '/api/posts', true);
    getPostsRequest.addEventListener('load', function() {
        callback(JSON.parse(getPostsRequest.responseText));
    });

    getPostsRequest.send();
}

// Because of proper naming, it’s easy to understand this code 
// without reading the actual functions
getUsers(function(users) {
    // Do something with users
});
getPosts(function(posts) {
    // Do something with posts
});

We can simplify the above code. Note how both functions are almost identical? We can apply the Don’t Repeat Yourself (DRY) principle. This prevents clutter.

function fetchJson(url, callback) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.addEventListener('load', function() {
        callback(JSON.parse(request.responseText));
    });

    request.send();
}

// The below code is still easy to understand 
// without reading the above function
fetchJson('/api/users', function(users) {
    // Do something with users
});
fetchJson('/api/posts', function(posts) {
    // Do something with posts
});

What if we want to create a new user through a POST request? At this point, one option is to add optional arguments to the function, introducing new logic to the function, making it too complex for one function. Another option is to create a new function specifically for POST requests, which would result in duplicate code.

We can get the best of both with object-oriented programming, allowing us to create a configurable single-use object, while keeping it maintainable.

Note: if you need a primer specifically on object-oriented JavaScript, I recommend this video: The Definitive Guide to Object-Oriented JavaScript

Object Oriented Programming

Consider objects, often called classes, a cluster of functions that are context-aware. An object fits beautifully in a dedicated file. In our case, we can build a basic wrapper for XMLHttpRequest.

HttpRequest.js

function HttpRequest(url) {
    this.request = new XMLHttpRequest();

    this.body = undefined;
    this.method = HttpRequest.METHOD_GET;
    this.url = url;

    this.responseParser = undefined;
}

HttpRequest.METHOD_GET = 'GET';
HttpRequest.METHOD_POST = 'POST';

HttpRequest.prototype.setMethod = function(method) {
    this.method = method;
    return this;
};

HttpRequest.prototype.setBody = function(body) {
    if (typeof body === 'object') {
        body = JSON.stringify(body);
    }

    this.body = body;
    return this;
};

HttpRequest.prototype.setResponseParser = function(responseParser) {
    if (typeof responseParser !== 'function') return;

    this.responseParser = responseParser;
    return this;
};

HttpRequest.prototype.send = function(callback) {
    this.request.addEventListener('load', function() {
        if (this.responseParser) {
            callback(this.responseParser(this.request.responseText));
        } else {
            callback(this.request.responseText);
        }
    }, false);

    this.request.open(this.method, this.url, true);
    this.request.send(this.body);
    return this;
};

app.js

new HttpRequest('/users')
    .setResponseParser(JSON.parse)
    .send(function(users) {
        // Do something with users
    });

new HttpRequest('/posts')
    .setResponseParser(JSON.parse)
    .send(function(posts) {
        // Do something with posts
    });

// Create a new user
new HttpRequest('/user')
    .setMethod(HttpRequest.METHOD_POST)
    .setBody({
        name: 'Tim',
        email: 'info@example.com'
    })
    .setResponseParser(JSON.parse)
    .send(function(user) {
        // Do something with new user
    });

The HttpRequest class created above is now very configurable, so can be applied for many of our API calls. Despite the implementation—a series of chained method calls—being more complex, the class’s features are easy to maintain. Finding a balance between implementation and reusability can be difficult and is project-specific.

When using OOP, design patterns make a great addition. Although they don’t improve readability per se, consistency does!

Human Syntax

Files, functions, objects, those are just the rough lines. They make your code easy to scan. Making code easy to read is a much more nuanced art. The tiniest detail can make a major difference. Limiting your line length to 80 characters, for example, is a simple solution that is often enforced by editors through a vertical line. But there’s more!

Naming

Appropriate naming can cause instant recognition, saving you the need to look up what a value is or what a function does.

Functions are usually in camel case. Starting them with a verb, followed by a subject often helps.

function getApiUrl() { /* ... */ }
function setRequestMethod() { /* ... */ }
function findItemsById(n) { /* ... */ }
function hideSearchForm() { /* ... */ }

For variable names, try to apply the inverted pyramid methodology. The subject comes first, properties come later.


var element = document.getElementById('body'),
    elementChildren = element.children,
    elementChildrenCount = elementChildren.length;

// When defining a set of colours, I prefix the variable with “color”
var colorBackground = 0xFAFAFA,
    colorPrimary = 0x663399;

// When defining a set of background properties, I use background as base
var backgroundColor = 0xFAFAFA,
    backgroundImages = ['foo.png', 'bar.png'];

// Context can make all the difference
var headerBackgroundColor = 0xFAFAFA,
    headerTextColor = 0x663399;

It’s also important being able to tell the difference between regular variables, and special ones. The name of constants, for example, are often written in uppercase and with underscores.

var URI_ROOT = window.location.href;

Classes are usually in camel case, starting with an uppercase letter.

function FooObject {
    // ...
}

A small detail is abbreviations. Some chose to write abbreviations in full uppercase while others choose to stick with camel case. Using the former may make it more difficult to recognize subsequent abbreviations.

Compactness and Optimisation

In many codebases, you may come across “special” code to reduce the number of characters, or to increase an algorithm’s performance.

A one-liner is an example of compact code. Unfortunately, they often rely on hacks or obscure syntax. A nested ternary operator, as seen below, is a common case. Despite being compact, it can also take a second or two to understand what it does, as opposed to regular if-statements. Be careful with syntactical shortcuts.

// Yay, someone managed to make this a one-liner!
var state = isHidden ? 'hidden' : isAnimating ? 'animating' : '';

// Yay, someone managed to make this readable!
var state = '';
if (isAnimating) state = 'animating';
if (isHidden) state = 'hidden';

Micro-optimisations are performance optimisations, often of little impact. Most of the time, they are less readable than a less performant equivalent.

// This may be most performant
$el[0].checked;

// But these are still fast, and are much easier to read
// Source: http://jsperf.com/prop-vs-ischecked/5
$el.prop('checked');
$el.is(':checked');
$el.attr('checked');

JavaScript compilers are really good in optimising code for us, and they keep getting better. Unless the difference between unoptimised and optimised code is noticeable, which often is after thousands or millions of operations, going for the easier read is recommended.

Non-Code

Call it irony, but a better way to keep code readable is to add syntax that isn’t executed. Let’s call it non-code.

Whitespace

I’m pretty sure every developer has had another developer supply, or has inspected a site’s minified code—code where most whitespace is removed. Coming across that the first time can be quite a surprise. In different visual artistic fields, like design and typography, void space is as important as fill. You will want to find the delicate balance between the two. Opinions on that balance vary per company, per team, per developer. Luckily, there are some universally agreed rules:

  • one expression per line,
  • indent the contents of a block,
  • an extra break can be used to separate sections of code.

Any other rule should be discussed with whoever you work with. Whatever code style you agree on, consistency is key.

function sendPostRequest(url, data, cb) {
    // A few assignments grouped together and neatly indented
    var requestMethod = 'POST',
        requestHeaders = {
            'Content-Type': 'text/plain'
        };

    // XMLHttpRequest initialisation, configuration and submission
    var request = new XMLHttpRequest();
    request.addEventListener('load', cb, false);
    request.open(requestMethod, url, false);
    request.send(data);
}

Comments

Much like whitespace, comments can be a great way to give your code some air, but also allows you to add details to code. Be sure to add comments to show:

  • explanation and argumentation of non-obvious code,
  • which bug or oddity a fix resolves, and sources when available.

// Sum values for the graph’s range
var sum = values.reduce(function(previousValue, currentValue) { 
    return previousValue + currentValue;
});

Not all fixes are obvious. Putting additional information can clarify a lot:

if ('addEventListener' in element) {
    element.addEventListener('click', myFunc);
}
// IE8 and lower do not support .addEventListener, 
// so .attachEvent should be used instead
// http://caniuse.com/#search=addEventListener
// https://msdn.microsoft.com/en-us/library/ms536343%28VS.85%29.aspx
else {
    element.attachEvent('click', myFunc);
}

Inline Documentation

When writing object-oriented software, inline docs can, much like regular comments, give some breathing space to your code. They also help clarify the purpose and details of a property or method. Many IDEs use them for hints, and generated documentation tools use them too! Whatever the reason is, writing docs is an excellent practise.

/**
 * Create a HTTP request
 * @constructor
 * @param {string} url
 */
function HttpRequest(url) {
    // ...
}

/**
 * Set an object of headers
 * @param {Object} headers
 * @return {HttpRequest}
 */
HttpRequest.prototype.setHeaders = function(headers) {
    for (var header in headers) {
        this.headers[header] = headers[header];
    }

    // Return self for chaining
    return this;
};

Callback Puzzles

Events and asynchronous calls are great JavaScript features, but it often makes code harder to read.

Async calls are often provided with callbacks. Sometimes, you want to run them in sequence, or wait for all of them to be ready.

function doRequest(url, success, error) { /* ... */ }

doRequest('https://example.com/api/users', function(users) {
    doRequest('https://example.com/api/posts', function(posts) {
        // Do something with users and posts
    }, function(error) {
        // /api/posts went wrong
    });
}, function(error) {
    // /api/users went wrong
});

The Promise object was introduced in ES2015 (also known as ES6) to solve both issues. It allows you to flatten down nested async requests.

function doRequest(url) {
    return new Promise(function(resolve, reject) {
        // Initialise request
        // Call resolve(response) on success
        // Call reject(error) on error
    });
}

// Request users first
doRequest('https://example.com/api/users')
// .then() is executed when they all executed successfully
.then(function(users) { /* ... */ })
// .catch() is executed when any of the promises fired the reject() function
.catch(function(error) { /* ... */ });

// Run multiple promises parallel
Promise.all([
    doRequest('https://example.com/api/users'),
    doRequest('https://example.com/api/posts')
])
.then(function(responses) { /* ... */ })
.catch(function(error) { /* ... */ });

Although we introduced additional code, this is easier to interpret correctly. You can read more about Promises here: JavaScript Goes Asynchronous (and It’s Awesome)

ES6/ES2015

If you are aware of the ES2015 spec, you may have noticed that all code examples in this article are of older versions (with the exception of the Promise object). Despite ES6 giving us great features, there are some concerns in terms of readability.

The fat arrow syntax defines a function that inherits the value of this from its parent scope. At least, that is why it was designed. It is tempting to use it to define regular functions as well.

var add = (a, b) => a + b;
console.log(add(1, 2)); // 3

Another example is the rest and spread syntax.

/**
 * Sums a list of numbers
 * @param {Array} numbers
 * @return {Number}
 */
function add(...numbers) {
    return n.reduce(function(previousValue, currentValue) {
        return previousValue + currentValue;
    }, 0);
}

add(...[1, 2, 3]);

/**
 * Sums a, b and c
 * @param {Number} a
 * @param {Number} b
 * @param {Number} c
 * @return {Number}
 */
function add(a, b, c) {
    return a + b + c;
}

add(1, 2, 3);

My point is that the ES2015 spec introduces a lot useful, but obscure, sometimes confusing syntax that lends itself to being abused for one-liners. I don’t want to discourage using these features. I want to encourage caution using them.

Conclusion

Keeping your code readable and maintainable is something to keep in mind at every stage of your project. From the file system to tiny syntactic choices, everything matters. Especially on teams, it’s hard to enforce all rules all the time. Code review can help, but still leaves room for human error. Luckily, there are tools to help you with that!

  • JSHint – a JavaScript linter to keep code error-free
  • Idiomatic – a popular code style standard, but feel free to deviate
  • EditorConfig – defining cross-editor code styles

Other than code quality and style tools, there are also tools that makes any code easier to read. Try different syntax highlight themes, or try a minimap to see a top-down overview of your script (Atom, Brackets).

What are your thoughts on writing readable and maintainable code? I’d love to hear them in the comments below.

  • Davide Borsatto

    I honestly think coding standards and meaningful code is the most important thing in our field. I hate having to work with messy code, sometimes my first commit is the result of some automatic beautifying tool. It’s not enough, but at least it’s not as ugly.

  • http://www.adriansandu.com Adrian SANDU

    I always dislike having to go through old code (even mine, never mind someone else’s). As a contractor I shift the environment often enough that I can’t settle down on a very strict personal coding standard. Unfortunately many of the places I’ve worked at don’t have a well defined coding standard either. They are making good steps at the current place but there is still a long way to go.

    • http://timseverien.nl/ Tim Severien

      I feel exactly the same. Refactoring existing code is an often underestimated task, especially when you’re expected to progressively improve it without breaking things. Having a good standard prior to starting a project removes the need to refactor design flaws, allowing focus on efficiency.

    • http://ChiefAlchemist.com/ Mark Simchock

      You’ve indirectly hit about a point missing in the article…code reviews. While maybe there might not be a defined (team) standard passing the eyes / minds of a couple+ others is probably the next best thing.

      • http://timseverien.nl/ Tim Severien

        You’re absolutely right! Code review is fantastic for many reasons, and code quality and readability is certainly among those.

  • http://www.nielsklom.eu Niels

    This is exactly why robots writing code is scary. Not because they are ‘better’, but because they will just do var1,2,3,4 and hence it will become unreadable for us to understand..

    • http://timseverien.nl/ Tim Severien

      If the editing is exclusively done by the robot/within the application, readability isn’t much of a problem. Code quality and efficiency is another story, however. Yey for humans!

  • M S i N Lund

    And remember to always put:
    // needed because browser x blows!
    … by all those lines of code of that will make no apparent sense later.

  • http://tassedecafe.org Jérémy Heleine

    Writing readable code is a more important thing than a beginner can think, thanks for sharing your best practices!

  • Steve Griffith

    In the Object Oriented Programming section, inside HttpRequest.js I think that this line:

    HttpRequest.prototype.setResponseParser = function(responseConverter) {

    Should be

    HttpRequest.prototype.setResponseParser = function(responseParser) {

    The variable responseParser is the one used inside the function.

    • http://timseverien.nl/ Tim Severien

      You have an eagle’s vision, sir! Fixed the code. Much appreciated!

  • http://ChiefAlchemist.com/ Mark Simchock

    I always try to ask myself:

    If someone else – with less time and experience with code than I have – looks at X are they going to love me or hate me?

    Code that isn’t maintainable (including comments) can never be great code. NEVER. Any one that says otherwise isn’t being honest and empathetic.

    Frankly, if you write for someone else in mind chances are good you’re going to be that person in 6+ months when you have to go back to something that you have since complete forgot about :)

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.