Skip to content

Latest commit

 

History

History
481 lines (392 loc) · 11.6 KB

File metadata and controls

481 lines (392 loc) · 11.6 KB

Error Handling - Clear vs Confusing

The Problem

Inconsistent error handling creates bugs and makes debugging impossible:

  • Mixed return types - Sometimes int, sometimes string, sometimes false
  • Silent failures - Code fails but nobody knows
  • No context - What went wrong? Where? Why?
  • Magic values - -1, 0, false, null - which means what?
  • Inconsistent patterns - Every function handles errors differently

Bad Practice (bad.php)

Problem 1: Mixed Return Types

function get_user_by_email($email) {
    if (!is_email($email)) {
        return false; // Invalid email
    }
    
    $user = get_user_by('email', $email);
    
    if (!$user) {
        return null; // User not found
    }
    
    return $user; // Success: WP_User object
}

// Usage - must check multiple types
$user = get_user_by_email($email);
if ($user === false) {
    // Handle invalid email
} elseif ($user === null) {
    // Handle not found
} else {
    // Use user
}

Problems:

  • Returns false, null, OR WP_User
  • Caller must know to check both false and null
  • Type hints impossible
  • Easy to make mistakes

Problem 2: Silent Failures

function update_user_meta_data($user_id, $data) {
    update_user_meta($user_id, 'first_name', $data['first_name']);
    update_user_meta($user_id, 'last_name', $data['last_name']);
    // If these fail... nobody knows!
}

Problems:

  • No error checking
  • No return value
  • Failures go unnoticed
  • Impossible to debug

Problem 3: The 0/false Bug

function find_position($id, $items) {
    foreach ($items as $index => $item) {
        if ($item->ID === $id) {
            return $index; // Could be 0!
        }
    }
    return false; // Not found
}

// BUG!
if (find_position($id, $items)) {
    // Never runs for position 0!
}

Problems:

  • 0 is falsy in PHP
  • if ($position) fails for index 0
  • Common source of bugs

Good Practice (good.php)

Solution 1: Exceptions for Exceptional Cases

function get_user_by_email( string $email ): WP_User {
    if ( ! is_email( $email ) ) {
        throw new InvalidEmailException( "Invalid email: {$email}" );
    }
    
    $user = get_user_by( 'email', $email );
    
    if ( ! $user instanceof WP_User ) {
        throw new UserNotFoundException( "User not found: {$email}" );
    }
    
    return $user; // Always returns WP_User
}

// Usage - clean and clear
try {
    $user = get_user_by_email( $email );
    // $user is guaranteed to be WP_User here
} catch ( InvalidEmailException $e ) {
    echo "Invalid email provided";
} catch ( UserNotFoundException $e ) {
    echo "User not found";
}

Benefits:

  • Always returns same type
  • Errors are exceptional flow
  • Type hints work perfectly
  • Clear error messages
  • Can't be ignored accidentally

Solution 2: WP_Error for WordPress Compatibility

function create_post( string $title, string $content ): int|WP_Error {
    if ( empty( $title ) ) {
        return new WP_Error(
            'empty_title',
            'Title is required',
            [ 'field' => 'title' ]
        );
    }
    
    $post_id = wp_insert_post(
        [
            'post_title'   => $title,
            'post_content' => $content,
        ]
    );
    
    if ( is_wp_error( $post_id ) ) {
        return new WP_Error(
            'post_creation_failed',
            'Failed to create post',
            [ 'original_error' => $post_id ]
        );
    }
    
    return $post_id; // int on success, WP_Error on failure
}

// Usage - consistent pattern
$result = create_post( $title, $content );

if ( is_wp_error( $result ) ) {
    echo "Error: " . $result->get_error_message();
    error_log( "Code: " . $result->get_error_code() );
} else {
    echo "Post ID: " . $result;
}

Benefits:

  • WordPress-native pattern
  • Error code, message, and data
  • Consistent across WordPress
  • Easy to check with is_wp_error()

Solution 3: Use null for "Not Found"

function find_post_position( int $post_id, array $posts ): ?int {
    foreach ( $posts as $index => $post ) {
        if ( $post->ID === $post_id ) {
            return $index; // Can be 0
        }
    }
    
    return null; // Not found
}

// Usage - no bug!
$position = find_post_position( $id, $posts );

if ( $position !== null ) {
    echo "Found at: " . $position; // Works for 0!
} else {
    echo "Not found";
}

Benefits:

  • null is never confused with 0
  • Explicit !== null check
  • No falsy value bugs

Solution 4: Result Objects for Complex Operations

class RegistrationResult {
    public function __construct(
        private readonly bool $success,
        private readonly ?int $user_id = null,
        private readonly array $errors = []
    ) {}
    
    public function is_successful(): bool {
        return $this->success;
    }
    
    public function get_user_id(): int {
        if ( ! $this->success ) {
            throw new LogicException( 'No user ID on failed registration' );
        }
        return $this->user_id;
    }
    
    public function get_errors(): array {
        return $this->errors;
    }
}

// Usage
$result = $service->register( $data );

if ( $result->is_successful() ) {
    echo "User ID: " . $result->get_user_id();
} else {
    foreach ( $result->get_errors() as $field => $error ) {
        echo "{$field}: {$error}";
    }
}

Benefits:

  • Explicit success/failure
  • Type-safe access to result data
  • Can include multiple error messages
  • Self-documenting

Solution 5: Custom Exception Types

class ImportException extends Exception {
    public function __construct(
        string $message,
        private readonly string $file_path,
        private readonly ?int $line_number = null
    ) {
        parent::__construct($message);
    }
    
    public function get_file_path(): string {
        return $this->file_path;
    }
    
    public function get_line_number(): ?int {
        return $this->line_number;
    }
}

// Usage
try {
    $importer->import( '/path/to/file.json' );
} catch ( ImportException $e ) {
    echo "Error: " . $e->getMessage();
    echo "File: " . $e->get_file_path();
    if ( $e->get_line_number() ) {
        echo "Line: " . $e->get_line_number();
    }
}

Benefits:

  • Rich error context
  • Type-specific catching
  • Additional error data

Solution 6: Enums for Error Codes (PHP 8.1+)

enum PermissionError: string {
    case NO_USER_ID = 'no_user_id';
    case USER_NOT_FOUND = 'user_not_found';
    case INSUFFICIENT_PERMISSIONS = 'insufficient_permissions';
    
    public function get_message(): string {
        return match( $this ) {
            self::NO_USER_ID => 'User ID required',
            self::USER_NOT_FOUND => 'User not found',
            self::INSUFFICIENT_PERMISSIONS => 'No permission',
        };
    }
}

class PermissionException extends Exception {
    public function __construct(
        private readonly PermissionError $error_code
    ) {
        parent::__construct( $error_code->get_message() );
    }
    
    public function get_error_code(): PermissionError {
        return $this->error_code;
    }
}

// Usage - type-safe error codes
try {
    check_permission( $user_id, 'edit_posts' );
} catch ( PermissionException $e ) {
    if ( $e->get_error_code() === PermissionError::USER_NOT_FOUND ) {
        // Handle specific error
    }
}

Benefits:

  • No magic strings
  • Type-safe error codes
  • IDE autocomplete
  • Compile-time checking

Error Handling Patterns

Pattern 1: Fail Fast

function process_data( array $data ): void {
    // Validate everything first
    if ( empty( $data['title'] ) ) {
        throw new ValidationException( 'Title required' );
    }
    
    if ( empty( $data['content'] ) ) {
        throw new ValidationException( 'Content required' );
    }
    
    // If we get here, data is valid
    $this->save( $data );
}

Pattern 2: Try-Catch for External Operations

function fetch_remote_data( string $url ): array {
    try {
        $response = wp_remote_get( $url );
        
        if ( is_wp_error( $response ) ) {
            throw new FetchException( $response->get_error_message() );
        }
        
        return json_decode( wp_remote_retrieve_body( $response ), true );
    } catch ( Exception $e ) {
        error_log( "Failed to fetch from {$url}: " . $e->getMessage() );
        throw new FetchException( "Remote fetch failed", 0, $e );
    }
}

Pattern 3: Validate Before Operating

class PostDeleter {
    public function delete( int $post_id ): void {
        // Validate first
        $this->validate_can_delete( $post_id );
        
        // Then delete
        $this->perform_deletion( $post_id );
    }
    
    private function validate_can_delete( int $post_id ): void {
        $post = get_post( $post_id );
        
        if ( ! $post ) {
            throw new PostNotFoundException( $post_id );
        }
        
        if ( ! current_user_can( 'delete_post', $post_id ) ) {
            throw new PermissionException( 'Cannot delete post' );
        }
    }
    
    private function perform_deletion( int $post_id ): void {
        // Deletion logic - no need to re-validate
    }
}

Pattern 4: Collect Errors, Don't Stop

class BatchImporter {
    public function import_all( array $items ): array {
        $successes = [];
        $errors    = [];
        
        foreach ( $items as $index => $item ) {
            try {
                $id           = $this->import_one( $item );
                $successes[] = $id;
            } catch ( Exception $e ) {
                $errors[ $index ] = $e->getMessage();
                // Continue with next item
            }
        }
        
        return [
            'imported' => $successes,
            'errors'   => $errors,
        ];
    }
}

When to Use What

Use Exceptions When:

  • ✅ Error is exceptional (not expected in normal flow)
  • ✅ Error should stop execution
  • ✅ You want to bubble errors up
  • ✅ You're NOT in WordPress core/plugin API

Use WP_Error When:

  • ✅ Building WordPress plugins/themes
  • ✅ Need WordPress compatibility
  • ✅ Error is part of normal flow (e.g., validation)
  • ✅ Want to provide multiple error messages

Use null When:

  • ✅ "Not found" is expected
  • ✅ Absence of value is valid
  • ✅ Avoids false/0 confusion

Use Result Objects When:

  • ✅ Operation can succeed with warnings
  • ✅ Need to return multiple pieces of information
  • ✅ Want explicit success/failure

Key Takeaways

Be consistent - Pick a pattern and stick to it
Fail fast - Validate early, fail loudly
Provide context - What failed? Why? Where?
Use null for not found - Avoid 0/false bugs
Don't swallow errors - Log or re-throw
Use type hints - Make return types clear

❌ Don't return different types on error
❌ Don't silently fail
❌ Don't use magic error codes
❌ Don't catch and ignore
❌ Don't use false for "not found"
❌ Don't mix error handling styles

The Bottom Line

Good error handling makes bugs impossible to ignore and easy to fix.

// BAD: Silent failure
update_meta( $id, $key, $value );
// Did it work? Who knows!

// GOOD: Explicit failure
$result = update_meta( $id, $key, $value );
if ( ! $result ) {
    throw new MetaUpdateException( "Failed to update {$key}" );
}

// BETTER: Return type tells the story
function update_meta( int $id, string $key, mixed $value ): void {
    $result = update_post_meta( $id, $key, $value );
    
    if ( $result === false ) {
        throw new MetaUpdateException( "Failed to update {$key} for post {$id}" );
    }
    // Void return = success guaranteed or exception thrown
}

Remember: Future you (debugging at 2am) will thank present you for good error handling!