DEV Community

loading...
Cover image for How I improved my code by returning early, returning often!

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

jordanfinners profile image Jordan Finneran Originally published at jordanfinners.dev Updated on ・3 min read

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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')
}
Enter fullscreen mode Exit fullscreen mode

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')
  }
}
Enter fullscreen mode Exit fullscreen mode

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?

Discussion (10)

Collapse
iquardt profile image
Iven Marquardt • Edited

Propagating NEVER use else doesn't make much sense for a post tagged with #functional, because it is synonymous with NEVER use FP. FP is about expressions that must evaluate to values, which obviously doesn't work if you drop the else branch. OOP on the other hand is about statements that evaluate to nothing but perform side effects. So expressions stand for pure values whereas statements stand for impure effects.

Regarding your example:

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

    result, err := someCalculation(data)

    return result, err
}
Enter fullscreen mode Exit fullscreen mode

It is not bad because you don't return early but because you rely on side effects in the first place. I think you made a really important observation but drew the wrong conclusion. Your example is simple and contrieved. Think about a huge program with side effects. I don't think that exiting early will save you in the long term but abandoning side effects will.

Collapse
jordanfinners profile image
Jordan Finneran Author

Thanks for the comment.
One of the way's I like to think about avoiding side effects is this returning thought process :)

Collapse
darkwiiplayer profile image
DarkWiiPlayer • Edited
func main() {
   data, err := loadData()
   result, err := someCalculation(data)
   return result, err
}

Now that code will work fine

No it won't? If loadData() fails, you will then discard that error by calling someCalculations and returning the wrong error to the caller. This code is objectively broken.

const validateAuthorisedPage = (page, isUserAuthenticated) => {
  const authenticatedPages = ['profile']
  if (authenticatedPages.includes(page) && isUserAuthenticated) {
    return page
  }
  return 'login'
}

Those two returns are on mutually exclusive separate branches, yet they're on different indentation levels. Neither of them is a guard clause. In conclusion, this code should be written like this instead:

function validateAuthorisedPage(page, isUserAuthenticated) {
   const authenticatedPages = ['profile']
   if (authenticatedPages.includes(page) && isUserAuthenticated) {
      return page
   } else {
      return 'login'
   }
}
Enter fullscreen mode Exit fullscreen mode

Suddenly it's clear that both returns are on different branches and only one of the two will run every time that code is executed.

Collapse
jordanfinners profile image
Jordan Finneran Author

Sorry for not making it clear, that first example is supposed to be bad as a demonstration. :)

It's good to see how others prefer to write that statement.
Thanks for the comments!

Collapse
darkwiiplayer profile image
DarkWiiPlayer

Sorry for not making it clear, that first example is supposed to be bad as a demonstration. :)

Yes, but you say that it's bad but not broken. I'm saying it is broken, not just bad.

Collapse
shogg profile image
shogg • Edited

The thing with pure functional programming is .. you can't have variables (persisted inner state). You can only chain function calls. You're not allowed to check a result twice.

Checking early doesn't make it more functional. But you are right, checking/returning early helps to prevent side effects and makes the code more clear.

Collapse
firozansari profile image
Firoz Ansari • Edited

Thank you.
This approach is also known as BouncerPattern:

Bouncer Pattern
The bouncer pattern

Collapse
darkwiiplayer profile image
DarkWiiPlayer

I know that pattern as guard clauses, if it's specifically used to rule out error cases at the beginning of the function.

Assertion is another word for it, but that can mean different things to different people.

The name Bouncer Pattern is new to me, but it is a good metaphor to describe what happens (It prevents unwanted input from "getting inside" the main program logic).

Collapse
jordanfinners profile image
Jordan Finneran Author

Thats good to know, I will have a read! Thank you!

Collapse
harshilparmar profile image
Harshil Parmar

Thanks bro !! Explanation which I am looking for...

Forem Open with the Forem app