How to improve code readability

It’s the first edition of my “Last Week” posts, where I don’t have any external links I want to share with you. So what I’ll do instead is give an example of typical JavaScript code and how I’d improve it to make it easier to understand.

Original Code

This is a function I reviewed some time ago. When reading the following code, try to analyse how you do it.

Are there certain lines where you have to think longer than others? Do you have to go back after reading a line? What makes you stop?

function renderResults(response) {
  hidePaginationArrows();

  let html;
  if (response.hits) {
    const results = response.hits.map(formatSearchResult);
    html = formatSearchTitle(`Results for: ${response.query}`);
    html += results.join("");
  } else {
    html = formatSearchTitle(`No results found for: ${searchInput.value}`);
  }

  documentationContent.innerHTML = html;
}

This code is okay. The naming of the variables is fine. It does what it is supposed to do and isn’t especially hard to understand.

I have written similar code and sometimes do on a first iteration. Since I got more into functional programming I always refactor code like this.

Let’s start improving it!

Remove unnecessary mutations

The first thing I’d change when refactoring code like this is the unnecessary use of mutation. It’s very easy to spot thanks to the let keyword.

Once you reach the line with this mutable variable, you have to keep this state in your head for the following lines. Due to the combination of mutability and a condition in the line below, your brain has to spend more cognitive energy to compare how each branch mutates this variable.

Let’s apply the “return early” pattern to improve this:

function renderResults(response) {
  hidePaginationArrows();

  if (response.hits) {
    documentationContent.innerHTML =
      formatSearchTitle(`Results for: ${response.query}`) +
      response.hits.map(formatSearchResult).join("");
    return;
  }

  documentationContent.innerHTML = formatSearchTitle(
    `No results found for: ${searchInput.value}`
  );
}

Notice how there is nothing to keep in your head anymore. One can read the code from top to bottom without comparing the different branches. The branch for the situation when response.hits exists ends the function execution with the return statement. The other branch follows below in case the condition evaluates to false.

This allows the reader to skip a branch that he/she does not care about. The possibility to skip code while reading is very important. Imagine having multiple branches in a function that you can safely skip while searching for the root cause of a bug.

Do one thing only

Looking at the body of the function one could notice that it does more than its name suggests. First it transforms some data to HTML and then it renders this HTML on the screen by appending it to the DOM.

Below I’ve split up this flow into two functions that only do what they say. I’ve also pulled out the hidePaginationArrows function, since it probably does something with the DOM as well.

Separating side effects from data transformation is one major principle in functional programming.

JavaScript has no type system or pure functions, so it is harder to figure out which functions have side effects. Any function could do more than using its inputs and returning a value. Mutating input values is also a side effect.

One clear indicator of impure functions are functions without a return value. These always have a side effect of some kind, e.g. calling some external library or mutating its inputs. Otherwise what is the point of calling a function, if it doesn’t do anything?

const html = getSearchResultsHtml(response);
hidePaginationArrows();
renderSearchResults(html);

function getSearchResultsHtml(response) {
  if (response.hits) {
    return (
      formatSearchTitle(`Results for: ${response.query}`) +
      response.hits.map(formatSearchResult).join("")
    );
  }
  return formatSearchTitle(`No results found for: ${searchInput.value}`);
}

function renderSearchResults(searchResultsHtml) {
  documentationContent.innerHTML = searchResultsHtml;
}

Note how you could stop reading the code after the first three lines to understand what it does from a high level perspective.

All major steps of the procedure are explicit now. Something else we’ve achieved is that one wouldn’t need to touch the render function if the format of the data changes. The separation of concerns has been improved.

Make dependencies explicit

The next thing I noticed is how getSearchResultsHtml uses the variable searchInput from “somewhere” (the current scope). It also probably displays the search term using different variables: response.query and searchInput.value.

renderSearchResults uses implicit imports too. We can change this function to require the HTML element as an input. This makes it a generic render function, which takes an element and html to render something on the screen.

const html = getSearchResultsHtml(searchInput.value, response.hits);
hidePaginationArrows();
render(documentationContent, html);

function getSearchResultsHtml(searchTerm, hits) {
  if (hits) {
    return (
      formatSearchTitle(`Results for: ${searchTerm}`) +
      hits.map(formatSearchResult).join("")
    );
  }
  return formatSearchTitle(`No results found for: ${searchTerm}`);
}

function render(element, html) {
  element.innerHTML = html;
}

The first three lines are now also explicit about the data and instances they depend on.

You might think about pulling out formatSearchResult from getSearchResultsHtml too. The question is what you’d achieve from it. Do you want to make it possible to replace the formatting of a single search result? What does the function do if you make this part configurable? How would you name it? I’ll leave this thought experiment to you.

Handle exceptions first

My final improvement is handling the exception first in getSearchResultsHtml. Exceptions tend to be shorter than the success case. The reader of this function can then skip the exception handling and quickly get to the main logic.

const html = getSearchResultsHtml(searchInput.value, response.hits);
hidePaginationArrows();
render(documentationContent, html);

function getSearchResultsHtml(searchTerm, hits) {
  if (!hits) {
    return formatSearchTitle(`No results found for: ${searchTerm}`);
  }
  return (
    formatSearchTitle(`Results for: ${searchTerm}`) +
    hits.map(formatSearchResult).join("")
  );
}

function render(element, html) {
  element.innerHTML = html;
}

Analysis of the final result

Compared to the original code at the top, the high level logic is explicit in the first three lines and separate from its implementation.

Data transformation has been separated from side effects. Dependencies are explicit and could be changed without touching the implementation inside of the functions. The code for conditionally visualising the search results is shorter. The render function is generalised and could be reused.

One could still think about better names for variables, functions or try to find out what hidePaginationArrows does, but I will stop here. This is an example to show how you can think about writing more readable code.

Keep in mind that the patterns I used are guidelines and not rules. As a programmer you’ll have to decide on a case by case basis if it makes sense to apply them. There are always advantages and disadvantages, even when refactoring code.

In general though I believe that in most cases these principles guide you towards writing code, that is easier to understand.