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
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, ORWP_User - Caller must know to check both false and null
- Type hints impossible
- Easy to make mistakes
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
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
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
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()
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
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
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
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
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 );
}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 );
}
}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
}
}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,
];
}
}- ✅ 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
- ✅ Building WordPress plugins/themes
- ✅ Need WordPress compatibility
- ✅ Error is part of normal flow (e.g., validation)
- ✅ Want to provide multiple error messages
- ✅ "Not found" is expected
- ✅ Absence of value is valid
- ✅ Avoids false/0 confusion
- ✅ Operation can succeed with warnings
- ✅ Need to return multiple pieces of information
- ✅ Want explicit success/failure
✅ 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
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!