WordPress WP_Error Class: Create, Check, Merge, and Return Structured Errors in Plugins

Error handling in PHP often defaults to returning false or null from a function that fails — leaving the caller with no information about why it failed. WordPress has its own structured error class, WP_Error, which carries one or more error codes, human-readable messages, and optional arbitrary data attached to each error. Every WordPress API that can fail returns either a meaningful value on success or a WP_Error on failure — wp_insert_post(), wp_remote_get(), wp_create_user(), and dozens more. The global helper is_wp_error() is the standard check. Knowing how to create, inspect, merge, and pass WP_Error objects makes your plugin code consistent with WordPress conventions, gives callers actionable error information, and integrates cleanly with the REST API error response system.

Problem: Your plugin's user-import function calls several WordPress APIs in sequence. When any step fails, callers receive a plain false with no explanation — making it impossible to display a useful admin notice or log which step failed.

Solution: Return a WP_Error on failure from each internal step. Check return values with is_wp_error(), merge errors when collecting multiple validation failures, and convert the final WP_Error to a REST API error response with WP_Error::as_error_response() when needed.

<?php
// ── Creating a WP_Error ───────────────────────────────────────────────
$error = new WP_Error(
    'import_failed',                          // error code (string)
    __( 'User import failed.', 'textdomain' ), // human-readable message
    [ 'user_email' => 'bad@example' ]          // optional data (any type)
);

// Add a second error to the same object (accumulate multiple failures)
$error->add(
    'missing_role',
    __( 'No role specified for import.', 'textdomain' )
);

// ── Checking for errors ───────────────────────────────────────────────
$result = wp_insert_user( $user_data );

if ( is_wp_error( $result ) ) {
    // get_error_code()    → first error code
    // get_error_codes()   → array of all codes
    // get_error_message() → first message (or message for a specific code)
    // get_error_messages()→ array of all messages
    // get_error_data()    → data attached to a specific code
    error_log( 'Insert failed: ' . $result->get_error_message() );
    foreach ( $result->get_error_codes() as $code ) {
        error_log( sprintf( '[%s] %s', $code, $result->get_error_message( $code ) ) );
    }
    return $result; // propagate up — callers use is_wp_error() too
}

// ── Merging errors from multiple validation steps ─────────────────────
function validate_import_row( array $row ): WP_Error|true {
    $errors = new WP_Error();

    if ( empty( $row['email'] ) || ! is_email( $row['email'] ) ) {
        $errors->add( 'invalid_email', __( 'Invalid email address.', 'textdomain' ) );
    }
    if ( empty( $row['username'] ) ) {
        $errors->add( 'missing_username', __( 'Username is required.', 'textdomain' ) );
    }

    return $errors->has_errors() ? $errors : true;
}

// ── WP_Error in a REST API endpoint ──────────────────────────────────
// WP REST API automatically converts WP_Error into a proper JSON error response
add_action( 'rest_api_init', function () {
    register_rest_route( 'my-plugin/v1', '/import', [
        'methods'             => 'POST',
        'callback'            => function ( WP_REST_Request $request ) {
            $result = run_import( $request->get_json_params() );
            if ( is_wp_error( $result ) ) {
                // REST API converts this to: {"code":"…","message":"…","data":{"status":422}}
                $result->add_data( [ 'status' => 422 ] );
                return $result;
            }
            return rest_ensure_response( [ 'imported' => $result ] );
        },
        'permission_callback' => fn() => current_user_can( 'manage_options' ),
    ] );
} );

NOTE: WP_Error can hold multiple error codes simultaneously — this is intentional for form validation scenarios where you want to surface all failures at once rather than one at a time. The has_errors() method (added in WP 5.1) is the cleanest way to check whether any errors have been added to an object you own. For the REST API, the data array on a WP_Error should include a 'status' key with an HTTP status code — the REST API reads this to set the response status code automatically. Without it, the REST API defaults to 500.