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.