Nested if statements create "arrow code" that's hard to read:
- Hard to follow - Logic is deeply nested
- Hard to find the happy path - What happens when everything is OK?
- Cognitive overload - Must track multiple indentation levels
- Difficult to modify - Adding conditions makes it worse
- Arrow anti-pattern - Code marches to the right
function update_post_meta_value( $post_id, $meta_key, $meta_value ) {
$post = get_post( $post_id );
if ( $post ) {
if ( $post->post_status === 'publish' ) {
if ( current_user_can( 'edit_post', $post_id ) ) {
$old_value = get_post_meta( $post_id, $meta_key, true );
if ( $old_value !== $meta_value ) {
$result = update_post_meta( $post_id, $meta_key, $meta_value );
if ( $result ) {
do_action( 'post_meta_updated', $post_id, $meta_key );
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
}Problems:
- 6 levels of indentation!
- The actual work is buried deep inside
- Hard to see what conditions must be met
- Lots of
else { return false; }noise
function update_post_meta_value( int $post_id, string $meta_key, mixed $meta_value ): bool {
$post = get_post( $post_id );
if ( ! $post ) {
return false;
}
if ( $post->post_status !== 'publish' ) {
return false;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return false;
}
$old_value = get_post_meta( $post_id, $meta_key, true );
if ( $old_value === $meta_value ) {
return false;
}
$result = update_post_meta( $post_id, $meta_key, $meta_value );
if ( ! $result ) {
return false;
}
do_action( 'post_meta_updated', $post_id, $meta_key );
return true;
}Benefits:
- All code at same indentation level
- Error conditions handled first
- Happy path is clear and linear
- Easy to add new validation
- Reads top-to-bottom like a checklist
if ( condition1 ) {
if ( condition2 ) {
if ( condition3 ) {
// Do the work
}
}
}if ( ! condition1 ) {
return;
}
if ( ! condition2 ) {
return;
}
if ( ! condition3 ) {
return;
}
// Do the workforeach ( $posts as $post ) {
if ( $post->post_status === 'publish' ) {
$meta = get_post_meta( $post->ID );
if ( ! empty( $meta ) ) {
foreach ( $meta as $key => $value ) {
if ( ! empty( $value[0] ) ) {
// Deep inside!
process( $key, $value[0] );
}
}
}
}
}foreach ( $posts as $post ) {
if ( $post->post_status !== 'publish' ) {
continue;
}
$meta = get_post_meta( $post->ID );
if ( empty( $meta ) ) {
continue;
}
foreach ( $meta as $key => $value ) {
if ( empty( $value[0] ) ) {
continue;
}
// Easy to find!
process( $key, $value[0] );
}
}function validate( $data ) {
if ( isset( $data['title'] ) ) {
if ( strlen( $data['title'] ) > 0 ) {
if ( strlen( $data['title'] ) <= 200 ) {
if ( isset( $data['content'] ) ) {
// More nesting...
return true;
}
}
}
}
return false;
}function validate( array $data ): bool {
if ( ! isset( $data['title'] ) ) {
return false;
}
if ( strlen( $data['title'] ) === 0 ) {
return false;
}
if ( strlen( $data['title'] ) > 200 ) {
return false;
}
if ( ! isset( $data['content'] ) ) {
return false;
}
return true;
}function validate( array $data ): bool|WP_Error {
if ( ! isset( $data['title'] ) ) {
return new WP_Error( 'missing_title', 'Title is required' );
}
if ( strlen( $data['title'] ) === 0 ) {
return new WP_Error( 'empty_title', 'Title cannot be empty' );
}
if ( strlen( $data['title'] ) > 200 ) {
return new WP_Error( 'title_too_long', 'Title must be 200 characters or less' );
}
if ( ! isset( $data['content'] ) ) {
return new WP_Error( 'missing_content', 'Content is required' );
}
return true;
}Sometimes you can combine related checks:
// Multiple early returns
if ( ! isset( $data['title'] ) ) {
return false;
}
if ( strlen( $data['title'] ) === 0 ) {
return false;
}
// Can be combined to:
if ( ! isset( $data['title'] ) || strlen( $data['title'] ) === 0 ) {
return false;
}
// Or even:
if ( empty( $data['title'] ) ) {
return false;
}But don't go too far:
// BAD: Too complex to read
if ( ! isset( $data['title'] ) || strlen( $data['title'] ) === 0 || strlen( $data['title'] ) > 200 || ! isset( $data['content'] ) ) {
return false;
}
// GOOD: Separate checks are clearer
if ( empty( $data['title'] ) ) {
return false;
}
if ( strlen( $data['title'] ) > 200 ) {
return false;
}
if ( ! isset( $data['content'] ) ) {
return false;
}For complex conditions, extract to variables:
// BAD: Hard to understand
if ( $post->post_status === 'publish' && ( $post->post_author == $user_id || in_array( 'administrator', $user->roles ) || ( in_array( 'editor', $user->roles ) && time() - strtotime( $post->post_date ) < 86400 ) ) ) {
// ...
}
// GOOD: Extract to readable variables
$is_published = $post->post_status === 'publish';
$is_author = $post->post_author == $user_id;
$is_admin = in_array( 'administrator', $user->roles );
$is_editor = in_array( 'editor', $user->roles );
$is_recent = time() - strtotime( $post->post_date ) < 86400;
$editor_can_edit = $is_editor && $is_recent;
if ( ! $is_published ) {
return false;
}
if ( ! ( $is_author || $is_admin || $editor_can_edit ) ) {
return false;
}
// Continue...Early returns work best for validation and error handling.
Don't use them when you need cleanup:
// BAD: Resource leak!
function process_file( $file_path ) {
$handle = fopen( $file_path, 'r' );
if ( ! $handle ) {
return false; // OK here
}
$data = fread( $handle, 1024 );
if ( empty( $data ) ) {
return false; // ✗ Didn't close handle!
}
fclose( $handle );
return $data;
}
// GOOD: Cleanup guaranteed
function process_file( $file_path ) {
$handle = fopen( $file_path, 'r' );
if ( ! $handle ) {
return false;
}
$data = fread( $handle, 1024 );
$result = ! empty( $data ) ? $data : false;
fclose( $handle );
return $result;
}
// BETTER: Use try-finally (PHP 5.5+)
function process_file( $file_path ) {
$handle = fopen( $file_path, 'r' );
if ( ! $handle ) {
return false;
}
try {
$data = fread( $handle, 1024 );
if ( empty( $data ) ) {
return false;
}
return $data;
} finally {
fclose( $handle );
}
}✅ Return early from error conditions
✅ Use guard clauses for validation
✅ Keep happy path at main indentation level
✅ Use continue to skip loop iterations
✅ Extract complex conditions to variables
❌ Don't nest if statements deeply
❌ Don't bury the happy path
❌ Don't forget cleanup when using early returns
❌ Don't make combined conditions too complex
Code should read like a list of requirements:
// Clear requirements list
function can_publish_post( int $post_id ): bool {
// Must have a post
$post = get_post( $post_id );
if ( ! $post ) {
return false;
}
// Must be in draft status
if ( $post->post_status !== 'draft' ) {
return false;
}
// Must have a title
if ( empty( $post->post_title ) ) {
return false;
}
// Must have content
if ( empty( $post->post_content ) ) {
return false;
}
// User must have permission
if ( ! current_user_can( 'publish_posts' ) ) {
return false;
}
// All checks passed!
return true;
}Each requirement is clear, and the happy ending is obvious: we return true!