Sign in
Log inSign up
A better alternative to “return early” – PHP / WordPress edition

A better alternative to “return early” – PHP / WordPress edition

Mike Schinkel's photo
Mike Schinkel
·Apr 23, 2019

This post was first published on my current blog.

Yes, complex conditionals are worse. But there is a third, better way. And no, it does not involve GOTOs. — Me

This post is about a coding pattern I have never seen used by anyone else for writing guard clauses), especially when those guard clauses are complex. This pattern results in code that is significantly more reliable, maintainable and easier to write than using either nested conditionals OR early returns.

Dogma status achieved

But first, let us review the cargo cultism of using early returns. Google return early pattern and you will find literally pages of advocacy for returning early. Here are examples from the first two (2) search result pages:

However, at least there is one (1) voice of reason, albeit without proposing a viable alternate:

Why do developers return early?

Frankly, the answer is simple. Returning early can result in code that is easier to read and follow compared with having only one return statement at the end of a function and using complex conditionals to enable that one return statement.

But that does not make returning early the best solution.

To illustrate how difficult code with conditional complex logic can be to comprehend by only reading it consider an example that does not return early from the WordPress core codebase. Below I have copied wp_setup_nav_menu_item() but with comments removed to make the code shorter.

(And yes, PHP developers who do not work with WordPress will almost certainly argue that WordPress’ codebase is suboptimal, but for the purpose of this example, that is exactly the point.)

I assume we can all agree that the method below is a disaster of complex logic? (But please do leave a comment if you think this code is easy to follow):

function wp_setup_nav_menu_item( $menu_item ) {
  if ( isset( $menu_item->post_type ) ) {
    if ( 'nav_menu_item' == $menu_item->post_type ) {
      $menu_item->db_id            = (int) $menu_item->ID;
      $menu_item->menu_item_parent = ! isset( $menu_item->menu_item_parent ) ? get_post_meta( $menu_item->ID, '_menu_item_menu_item_parent', true ) : $menu_item->menu_item_parent;
      $menu_item->object_id        = ! isset( $menu_item->object_id ) ? get_post_meta( $menu_item->ID, '_menu_item_object_id', true ) : $menu_item->object_id;
      $menu_item->object           = ! isset( $menu_item->object ) ? get_post_meta( $menu_item->ID, '_menu_item_object', true ) : $menu_item->object;
      $menu_item->type             = ! isset( $menu_item->type ) ? get_post_meta( $menu_item->ID, '_menu_item_type', true ) : $menu_item->type;
      if ( 'post_type' == $menu_item->type ) {
        $object = get_post_type_object( $menu_item->object );
        if ( $object ) {
          $menu_item->type_label = $object->labels->singular_name;
        } else {
          $menu_item->type_label = $menu_item->object;
          $menu_item->_invalid   = true;
        }
        if ( 'trash' === get_post_status( $menu_item->object_id ) ) {
          $menu_item->_invalid = true;
        }
        $menu_item->url  = get_permalink( $menu_item->object_id );
        $original_object = get_post( $menu_item->object_id );
        /** This filter is documented in wp-includes/post-template.php */
        $original_title = apply_filters( 'the_title', $original_object->post_title, $original_object->ID );
        if ( '' === $original_title ) {
          /* translators: %d: ID of a post */
          $original_title = sprintf( __( '#%d (no title)' ), $original_object->ID );
        }
        $menu_item->title = '' == $menu_item->post_title ? $original_title : $menu_item->post_title;
      } elseif ( 'post_type_archive' == $menu_item->type ) {
        $object = get_post_type_object( $menu_item->object );
        if ( $object ) {
          $menu_item->title      = '' == $menu_item->post_title ? $object->labels->archives : $menu_item->post_title;
          $post_type_description = $object->description;
        } else {
          $menu_item->_invalid   = true;
          $post_type_description = '';
        }
        $menu_item->type_label = __( 'Post Type Archive' );
        $post_content          = wp_trim_words( $menu_item->post_content, 200 );
        $post_type_description = '' == $post_content ? $post_type_description : $post_content;
        $menu_item->url        = get_post_type_archive_link( $menu_item->object );
      } elseif ( 'taxonomy' == $menu_item->type ) {
        $object = get_taxonomy( $menu_item->object );
        if ( $object ) {
          $menu_item->type_label = $object->labels->singular_name;
        } else {
          $menu_item->type_label = $menu_item->object;
          $menu_item->_invalid   = true;
        }
        $term_url       = get_term_link( (int) $menu_item->object_id, $menu_item->object );
        $menu_item->url = ! is_wp_error( $term_url ) ? $term_url : '';
        $original_title = get_term_field( 'name', $menu_item->object_id, $menu_item->object, 'raw' );
        if ( is_wp_error( $original_title ) ) {
          $original_title = false;
        }
        $menu_item->title = '' == $menu_item->post_title ? $original_title : $menu_item->post_title;
      } else {
        $menu_item->type_label = __( 'Custom Link' );
        $menu_item->title      = $menu_item->post_title;
        $menu_item->url        = ! isset( $menu_item->url ) ? get_post_meta( $menu_item->ID, '_menu_item_url', true ) : $menu_item->url;
      }
      $menu_item->target     = ! isset( $menu_item->target ) ? get_post_meta( $menu_item->ID, '_menu_item_target', true ) : $menu_item->target;
      $menu_item->attr_title = ! isset( $menu_item->attr_title ) ? apply_filters( 'nav_menu_attr_title', $menu_item->post_excerpt ) : $menu_item->attr_title;
      if ( ! isset( $menu_item->description ) ) {
        $menu_item->description = apply_filters( 'nav_menu_description', wp_trim_words( $menu_item->post_content, 200 ) );
      }
      $menu_item->classes = ! isset( $menu_item->classes ) ? (array) get_post_meta( $menu_item->ID, '_menu_item_classes', true ) : $menu_item->classes;
      $menu_item->xfn     = ! isset( $menu_item->xfn ) ? get_post_meta( $menu_item->ID, '_menu_item_xfn', true ) : $menu_item->xfn;
    } else {
      $menu_item->db_id            = 0;
      $menu_item->menu_item_parent = 0;
      $menu_item->object_id        = (int) $menu_item->ID;
      $menu_item->type             = 'post_type';
      $object                = get_post_type_object( $menu_item->post_type );
      $menu_item->object     = $object->name;
      $menu_item->type_label = $object->labels->singular_name;
      if ( '' === $menu_item->post_title ) {
        $menu_item->post_title = sprintf( __( '#%d (no title)' ), $menu_item->ID );
      }
      $menu_item->title       = $menu_item->post_title;
      $menu_item->url         = get_permalink( $menu_item->ID );
      $menu_item->target      = '';
      $menu_item->attr_title  = apply_filters( 'nav_menu_attr_title', '' );
      $menu_item->description = apply_filters( 'nav_menu_description', '' );
      $menu_item->classes     = array();
      $menu_item->xfn         = '';
    }
  } elseif ( isset( $menu_item->taxonomy ) ) {
    $menu_item->ID               = $menu_item->term_id;
    $menu_item->db_id            = 0;
    $menu_item->menu_item_parent = 0;
    $menu_item->object_id        = (int) $menu_item->term_id;
    $menu_item->post_parent      = (int) $menu_item->parent;
    $menu_item->type             = 'taxonomy';

    $object                = get_taxonomy( $menu_item->taxonomy );
    $menu_item->object     = $object->name;
    $menu_item->type_label = $object->labels->singular_name;

    $menu_item->title       = $menu_item->name;
    $menu_item->url         = get_term_link( $menu_item, $menu_item->taxonomy );
    $menu_item->target      = '';
    $menu_item->attr_title  = '';
    $menu_item->description = get_term_field( 'description', $menu_item->term_id, $menu_item->taxonomy );
    $menu_item->classes     = array();
    $menu_item->xfn         = '';
  }
  return apply_filters( 'wp_setup_nav_menu_item', $menu_item );
}

Refactoring wp_setup_nav_menu_item() to return early

Let us first refactor to return early to illustrate the difference. If we collapse the above code down we get the following:

function wp_setup_nav_menu_item( $menu_item ) {
  if ( isset( $menu_item->post_type ) ) {
    if ( 'nav_menu_item' == $menu_item->post_type ) {
      ...
    } else  {
      ...
    }
  } elseif ( isset( $menu_item->taxonomy ) ) {
    ...
  }
  return apply_filters( 'wp_setup_nav_menu_item', $menu_item );
}

Refactoring the above to allow us to use early returns gives us the following:

function wp_setup_nav_menu_item( $menu_item ) {
  if ( isset( $menu_item->taxonomy ) ) {
    ...
    return;
  }
  if ( ! isset( $menu_item->post_type ) ) {
    return;
  }
  if ( 'nav_menu_item' == $menu_item->post_type ) {
    ...
    return;
  }
  ...
  return;
}

Our refactored early return code is much easier to understand at a glance, right?

(Note that I happen to know that $menu_item->post_type and $menu_item->taxonomy are mutually exclusive so it is okay that I swapped them, otherwise my refactor my produce a different result than the original code.)

Unfortunately I omitted one detail in the above refactored code. That code did not include apply_filters() on the return statement, which is included in the next example. Hopefully you will agree that this code is not as easily to follow — and probably why WordPress core did not use early returns — but is arguably still easier to grok than the original version:

function wp_setup_nav_menu_item( $menu_item ) {
  if ( isset( $menu_item->taxonomy ) ) {
    ...
    return apply_filters( 'wp_setup_nav_menu_item', $menu_item );
  }
  if ( ! isset( $menu_item->post_type ) ) {
    return apply_filters( 'wp_setup_nav_menu_item', $menu_item );
  }
  if ( 'nav_menu_item' == $menu_item->post_type ) {
    ...
    return apply_filters( 'wp_setup_nav_menu_item', $menu_item );
  }
  ...
  return apply_filters( 'wp_setup_nav_menu_item', $menu_item );
}

The above code does commit one unforgivable sin in the eyes of many developers; it violates the “dreaded” D.R.Y. principle, and in this case they would be right to be concerned.

What is wrong with returning early?

Thus far in this post I have only given you positive reasons for using early returns. So what are the negatives?

1. Multiple return points

Avoiding multiple return points in a function/method is the most common argument against returning early, even though some argue the old admonition to always have one exit point in a subroutine is old-school thinking and that returning early is more enlightened than those old school laggards.

But — and this is a true <rant> for me — I will argue it is almost borderline sociopathic for developers to return early because of how the pattern negatively affects the debugging sessions run by future developers trying to either learn or maintain the code. In the case of trying to grok a codebase one of the techniques is to set breakpoints in code and debug into the code and then “look around” to witness the call tree and values of variables.

Still, and I cannot state this emphatically enough, it is infuriating to set a breakpoint on a return statement, run the debugger, and have it continue beyond the intended breakpoint because some jerk programmer decided to return early in the function being breakpointed.

This is not a mere inconvenience. Multiple returns in code often takes significantly longer for the developer debugging the code to track down and breakpoint all the potentially relevant return statements, restart the debugger, and then repeat the process until they are able to understand what they were trying to understand in the first place. Not to mention the mental disruption of having to figure out why the debugger was not behaving at first as they expected it to. </rant>

2. Inability to execute common final or “cleanup” code

While developers who do not use debuggers will often rationalize the prior point as being the other developer’s problem (I did mention it was borderline sociopathic behavior, didn’t I?) this second reason to avoid early returns affects everyone.

Sure, not every function will need shared exit code, but when it is needed you will either have to abandon the pattern or refactor the code to allow it to more easily share common exit code, which may or may not be the ideal way to structure your code. In my experience, having used my proposed alternative for several years now, most functions that appear not to need common exit code at first will eventually evolve to have such a need.

It is best to start with a pattern you won’t have to change significantly — to avoid introducing bugs — as the code evolves, and the early return pattern is not that pattern.

3. Early returns provide only one bypass per function

While this may be harder to understand at first, once you start using the alternative I propose I submit you will soon discover how limiting it can be do not be able to use an early bypass branch more than once per function or method. So without further ado…

Why not use a bypass with no real downsides?

Instead of accepting the downsides of the early return, why not use a pattern that has no real downsides? Why not use a pattern that:

  • Has a single exit point per function/method,
  • Eliminates complex conditional logic,
  • Allows bypassing unnecessary code, just like the early return,
  • Can be employed more than once per function/method, and
  • Supports running common exit code cleanly.

Introducing: The Break/Continue Guard Clause pattern

Instead of using early returns, use PHP’s existing control structures — the do and for loops — and then write guard clauses that use break/continue to bypass code when said code does not need to be executed.

The most common subpattern I had found from this pattern leverages break with do {...} while ( false ); and it looks like this:

function example() {
  do {
    $value = null;  // Initialize your default return value
    ...
    if ( some_guard_clause ) {
       break;
    }
    ...
    if ( some_other_guard_clause ) {
       break;
    }
    ...
    // Run the guarded use-case code here
    ...
  } while ( false );
  ...
  // run common code here
  ...
  return $value
}

Refactoring with do{...}while(false) and break

Now let us revisit our earlier example from WordPress core of refactoring the top level logic of wp_setup_nav_menu_item() but this time using break in conjunction with do {...} while (false):

function wp_setup_nav_menu_item( $menu_item ) {
  do {
    if ( isset( $menu_item->taxonomy ) ) {
      ...
      break;
    }
    if ( ! isset( $menu_item->post_type ) ) {
      break;
    }
    if ( 'nav_menu_item' == $menu_item->post_type ) {
      ...
      break;
    }
    ...
  while ( false );
  ...
  return apply_filters( 'wp_setup_nav_menu_item', $menu_item );
}

The Final Example

This time let us use some concrete code. Assuming I refactored most of the function’s code into other functions the above logic might look like this:

function wp_setup_nav_menu_item( $menu_item ) {
  do {
    if ( isset( $menu_item->taxonomy ) ) {
      _wp_setup_taxonomy_nav_menu_item( $menu_item );
      break;
    }
    if ( ! isset( $menu_item->post_type ) ) {
      break;
    }
    if ( 'nav_menu_item' !== $menu_item->post_type ) {
      _wp_setup_post_type_nav_menu_item( $menu_item );
      break;
    }
    _wp_setup_nav_menu_item( $menu_item );
  } while ( false );
  return apply_filters( 'wp_setup_nav_menu_item', $menu_item );
}

Hopefully you will concur that the above code is the easiest to follow of all the code presented thus far on this blog post?

And, for posterity, here is the rest of the refactored bits of wp_setup_nav_menu_item():

function _wp_setup_nav_menu_item( $menu_item ) {
  $menu_item->db_id = (int) $menu_item->ID;
  $menu_item->menu_item_parent = ! isset( $menu_item->menu_item_parent )
    ? get_post_meta( $menu_item->ID, '_menu_item_menu_item_parent', true )
    : $menu_item->menu_item_parent;
  $menu_item->object_id = ! isset( $menu_item->object_id )
    ? get_post_meta( $menu_item->ID, '_menu_item_object_id', true )
    : $menu_item->object_id;
  $menu_item->object = ! isset( $menu_item->object )
    ? get_post_meta( $menu_item->ID, '_menu_item_object', true )
    : $menu_item->object;
  $menu_item->type = ! isset( $menu_item->type )
    ? get_post_meta( $menu_item->ID, '_menu_item_type', true )
    : $menu_item->type;

  switch ( $menu_item->type ) {
    case 'post_type':
      _wp_setup_nav_menu_post_type_item( $menu_item );
      break;

    case 'post_type_archive':
      _wp_setup_nav_menu_post_type_archive_item( $menu_item );
      break;

    case 'taxonomy':
      _wp_setup_nav_taxonomy_item( $menu_item );
      break;

    default:
      _wp_setup_nav_default_item( $menu_item );
      break;
  }

  $menu_item->target = ! isset( $menu_item->target )
    ? get_post_meta( $menu_item->ID, '_menu_item_target', true )
    : $menu_item->target;
  $menu_item->attr_title = ! isset( $menu_item->attr_title )
    ? apply_filters( 'nav_menu_attr_title', $menu_item->post_excerpt )
    : $menu_item->attr_title;

  if ( ! isset( $menu_item->description ) ) {
    $menu_item->description = apply_filters( 'nav_menu_description',
      wp_trim_words( $menu_item->post_content, 200 )
    );
  }
  $menu_item->classes = ! isset( $menu_item->classes )
    ? (array) get_post_meta( $menu_item->ID, '_menu_item_classes', true )
    : $menu_item->classes;

  $menu_item->xfn = ! isset( $menu_item->xfn )
    ? get_post_meta( $menu_item->ID, '_menu_item_xfn', true )
    : $menu_item->xfn;
}

function _wp_setup_nav_menu_post_type_item( $menu_item ) {
  $object = get_post_type_object( $menu_item->object );
  do {
    if ( $object ) {
      $menu_item->type_label = $object->labels->singular_name;
      break;
    }
    $menu_item->type_label = $menu_item->object;
    $menu_item->_invalid = true;
  } while ( false );
  if ( 'trash' === get_post_status( $menu_item->object_id ) ) {
    $menu_item->_invalid = true;
  }
  $menu_item->url = get_permalink( $menu_item->object_id );
  $original_object = get_post( $menu_item->object_id );
  _/ This filter is documented in wp-includes/post-template.php */_ $original_title = apply_filters( 'the_title',
    $original_object->post_title,
    $original_object->ID );
  if ( '' === $original_title ) {
    _/* translators: %d: ID of a post */_ $original_title = _sprintf_( __( '#%d (no title)' ), $original_object->ID );
  }
  $menu_item->title = '' == $menu_item->post_title ? $original_title
    : $menu_item->post_title;
}

function _wp_setup_nav_menu_post_type_archive_item( $menu_item ) {
  $object = get_post_type_object( $menu_item->object );
  do {
    if ( $object ) {
      $menu_item->title = '' == $menu_item->post_title ? $object->labels->archives : $menu_item->post_title;
      $post_type_description = $object->description;
      break;
    }
    $menu_item->_invalid = true;
    $post_type_description = '';
  } while ( false );
  $menu_item->type_label = __( 'Post Type Archive' );
  $post_content          = wp_trim_words( $menu_item->post_content, 200 );
  $post_type_description = '' == $post_content
    ? $post_type_description
    : $post_content;
  $menu_item->url = get_post_type_archive_link( $menu_item->object );
}

function _wp_setup_nav_taxonomy_item( $menu_item ) {
  $object = get_taxonomy( $menu_item->object );
  if ( $object ) {
    $menu_item->type_label = $object->labels->singular_name;
  } else {
    $menu_item->type_label = $menu_item->object;
    $menu_item->_invalid = true;
  }
  $term_url = get_term_link( (int) $menu_item->object_id, $menu_item->object );
  $menu_item->url = ! is_wp_error( $term_url ) ? $term_url : '';
  $original_title = get_term_field( 'name',
    $menu_item->object_id,
    $menu_item->object,
 'raw'
  );
  if ( is_wp_error( $original_title ) ) {
    $original_title = false;
  }
  $menu_item->title = '' == $menu_item->post_title ? $original_title
    : $menu_item->post_title;
}

function _wp_setup_nav_default_item( $menu_item ) {
  $menu_item->type_label = __( 'Custom Link' );
  $menu_item->title = $menu_item->post_title;
  $menu_item->url = ! isset( $menu_item->url )
    ? get_post_meta( $menu_item->ID, '_menu_item_url', true )
    : $menu_item->url;
}

function _wp_setup_post_type_nav_menu_item( $menu_item ) {
  $menu_item->db_id = 0;
  $menu_item->menu_item_parent = 0;
  $menu_item->object_id = (int) $menu_item->ID;
  $menu_item->type = 'post_type';
  $object                      = get_post_type_object( $menu_item->post_type );
  $menu_item->object = $object->name;
  $menu_item->type_label = $object->labels->singular_name;
  if ( '' === $menu_item->post_title ) {
    $menu_item->post_title = _sprintf_( __( '#%d (no title)' ), $menu_item->ID );
  }
  $menu_item->title = $menu_item->post_title;
  $menu_item->url = get_permalink( $menu_item->ID );
  $menu_item->target = '';
  $menu_item->attr_title = apply_filters( 'nav_menu_attr_title', '' );
  $menu_item->description = apply_filters( 'nav_menu_description', '' );
  $menu_item->classes = array();
  $menu_item->xfn = '';
}

function _wp_setup_taxonomy_nav_menu_item( $menu_item ) {
  $menu_item->ID = $menu_item->term_id;
  $menu_item->db_id = 0;
  $menu_item->menu_item_parent = 0;
  $menu_item->object_id = (int) $menu_item->term_id;
  $menu_item->post_parent = (int) $menu_item->parent;
  $menu_item->type = 'taxonomy';

  $object                = get_taxonomy( $menu_item->taxonomy );
  $menu_item->object = $object->name;
  $menu_item->type_label = $object->labels->singular_name;

  $menu_item->title = $menu_item->name;
  $menu_item->url = get_term_link( $menu_item, $menu_item->taxonomy );
  $menu_item->target = '';
  $menu_item->attr_title = '';
  $menu_item->description = get_term_field( 'description',
    $menu_item->term_id,
    $menu_item->taxonomy
  );
  $menu_item->classes = array();
  $menu_item->xfn = '';
}

In closing, I hope that you will seriously consider using this pattern instead of the “early return” pattern, if not for yourself but instead for those programmers who, in the future, will be in the position to maintain the code you write today.

Hassle-free blogging platform that developers and teams love.
  • Docs by Hashnode
    New
  • Blogs
  • AI Markdown Editor
  • GraphQL APIs
  • Open source Starter-kit

© Hashnode 2024 — LinearBytes Inc.

Privacy PolicyTermsCode of Conduct