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.
Frequently Asked Questions about Readable Code
Why is it important for code to be readable by humans?
The readability of code is crucial for several reasons. Firstly, it makes the code easier to understand, debug, and maintain. When code is readable, it’s easier for other developers to understand what the code is doing, which is particularly important in collaborative environments. Secondly, readable code is more likely to be correct. If a developer can easily understand the code, they are less likely to introduce bugs when they modify it. Lastly, readable code is easier to test. If the code is clear and concise, it’s easier to identify what needs to be tested and how to test it.
What makes a programming language easy to read?
A programming language is considered easy to read if it has a clear and concise syntax, uses meaningful identifiers, and includes comments that explain what the code is doing. High-level languages like Python and Ruby are often considered easy to read because they use English-like syntax and allow for clear, descriptive variable names. However, readability can also be improved in lower-level languages like C++ or Java through good coding practices, such as consistent indentation, use of whitespace, and comprehensive comments.
How can functions reduce the amount of code?
Functions can significantly reduce the amount of code by allowing developers to reuse code. Instead of writing the same code multiple times, a function can be written once and then called whenever that particular task needs to be performed. This not only makes the code shorter and easier to read, but also makes it easier to maintain and debug, as any changes only need to be made in one place.
What is the difference between machine code and high-level languages?
Machine code is the lowest level of programming language, consisting of binary code that can be directly executed by a computer’s central processing unit (CPU). High-level languages, on the other hand, are closer to human languages and need to be translated into machine code by compilers or interpreters before they can be executed. High-level languages are generally easier to read and write, and they provide more abstraction from the hardware, making them more portable across different types of machines.
How do interpreters and compilers work?
Interpreters and compilers are tools that translate high-level languages into machine code. An interpreter translates and executes code line by line, which allows for interactive coding and debugging. However, this can be slower than compiled code. A compiler, on the other hand, translates the entire program into machine code before execution, which can result in faster execution times. However, any errors in the code will not be discovered until the entire program is compiled.
What is assembly language?
Assembly language is a type of low-level programming language that uses mnemonic codes to represent machine code instructions. Each assembly language is specific to a particular computer architecture. While it is more readable than machine code, it is still much more difficult to read and write than high-level languages. However, it allows for direct control over the hardware, which can be useful in certain situations.
How can I improve the readability of my code?
There are several ways to improve the readability of your code. These include using meaningful variable and function names, consistently indenting your code, using whitespace to separate different sections of your code, and including comments that explain what your code is doing. It’s also important to follow the conventions and best practices of the programming language you are using.
What is the role of comments in making code readable?
Comments play a crucial role in making code readable. They provide explanations of what the code is doing, why certain decisions were made, and how complex sections of code work. This can be extremely helpful for other developers who need to understand and work with your code. However, it’s important to keep comments concise and relevant, and to update them as the code changes.
How does readable code affect collaboration?
Readable code greatly facilitates collaboration. When code is easy to read, it’s easier for other developers to understand and contribute to. This is particularly important in large projects where multiple developers are working on different parts of the codebase. Readable code also makes it easier to onboard new team members, as they can quickly understand what the code is doing and how it works.
How does readable code affect software quality?
Readable code can significantly improve software quality. When code is easy to read, it’s easier to spot and fix bugs, and to ensure that the code is doing what it’s supposed to do. It also makes it easier to maintain and enhance the software over time, as it’s clear what each part of the code is doing. This can lead to more reliable, efficient, and robust software.
Tim Severien is an enthusiastic front-end developer from the Netherlands, passionate about JavaScript and Sass. When not writing code, he write articles for SitePoint or for Tim’s blog.