How I improved my code by returning early, returning often!

Contents

  1. Intro
  2. Return
  3. Single Purpose Functions
  4. Summary

Intro

I've been a developer for over 5 years now and one of the best things that I've learned is functional programming. Which gets a lot of hype and can be a bit daunting but I've broken down into a few simple ideas:

  • Returning early and often
  • Single purpose functions

These are pretty tightly coupled and inspired by my friends post (which you should definitely check out) about NEVER using ELSE.

Return

Here's an example in Go. We'll load some data, do some work on the data and return the result. Loading data and doing some calculation could both return an error as well as the actual thing we want.

func main() {
	data, err := loadData()

	result, err := someCalculation(data)

	return result, err
}

Now that code will run fine, however if there is an error from load data and doing the calculation, we'll only ever see the second error as it will override the original error.

A nightmare to debug!

Not only that but we'll also be doing extra computation we don't need!

We can fix it up by checking for error and returning that early.

func main() {
	data, err := loadData()

	if err != nil {
		return nil, err
	}

	result, err := someCalculation(data)

	if err != nil {
		return nil, err
	}

	return result, nil
}

This will save us doing any extra computation unnecessarily and gives us context if any error happens. This second code block could be improved further with proper logging too.

It'll be much easier to debug when something goes wrong too!

Single Purpose Functions

Returning early and often also helps lead us to functions with only a single purpose.

Let's take the following example of some routing in JavaScript. Imagine we're parsing the URL e.g. /:page Based on the page import some code. We also could have no page value set if someone goes to just /. We also only want to load the profile code if a user is authenticated.

You can see its pretty complex to read and already wrong as it is missing an else and we're not returning anything so could lead to some mutations.

if (!page || page === 'home') {
  import('./home.js')
} else if (page === 'blog') {
  import('./blog.js')
} else if (page === 'login') {
  import('./login.js')
} 
if (page === 'profile' && isUserAuthenticated) {
  import('./profile.js')
} else {
  import('./lost.js')
}

Let's break it out into single purpose functions!

We'll start by checking if the page is known to us. Then check if the page needs authentication and if the user is logged in. Finally, we'll import the write code depending on the page.

/**
 * Check if the page is a known page
 * Default to home page if route is just /
 * Otherwise show lost page
 * @param {String} page the page parsed from the url
 * @returns {String} validated page to go to
 */
const validatePage = (page) => {
  if (!page) {
    return 'home'
  }
  if (['profile', 'blog', 'login'].includes(page)) {
    return page
  }
  return 'lost'
}

/**
 * Check if the page is authorised and we have a user logged in
 * Otherwise, they need to login
 * @param {String} page the validated page
 * @param {Boolean} isUserAuthenticated if the user is logged in
 * @returns {String} the page to go to 
 */
const validateAuthorisedPage = (page, isUserAuthenticated) => {
  const authenticatedPages = ['profile']
  if (authenticatedPages.includes(page) && isUserAuthenticated) {
    return page
  }
  return 'login'
}

/**
 * Import the right code for each page
 * @param {String} page to load
 * @returns {Promise} the pending import
 */
const importPage = async (page) => {
  switch (page) {
    case 'home':
      return import('./home.js')
    case 'blog':
      return import('./blog.js')
    case 'profile':
      return import('./profile.js')
    case 'login':
      return import('./login.js')
    default:
      return import('./lost.js')
  }
}

You can see that each of these is only responsible for doing one thing! It also takes advantage of returning early and often too. This makes it easier to read, understand, and makes testing a breeze!

Summary

In summary, mutation is the enemy!

Thinking about returning as early as possible helps keep our code simple, leads to easier error handling and less likely for side effects to occur!

What do you think? Any other tips for simpler code?

These are webmentions powered by webmention.io