- What username_exists() Returns
- Basic Usage of username_exists()
- Add a Live AJAX Availability Check
- The username_exists Filter
- Use the Filter to Reserve Usernames
- Common Mistakes to Avoid
- FAQs About username_exists
- Does username_exists() return true or false?
- Is username_exists() case-sensitive?
- Should I sanitize the username before checking?
- Can I block usernames without writing to the database?
- Conclusion
You built a registration form, wired it up, and now half of your users are picking usernames that already belong to somebody else. WordPress stores user_login values in a single globally unique column, so a duplicate is not a soft warning — it is a failed insert and a broken signup flow.
That is what username_exists is for. It is a core WordPress function that takes a username, queries the users table, and hands you back the user ID when the name is taken. In this guide you will learn what it returns, how to use it in a custom form, how to wire it into a live AJAX check, and how the username_exists filter lets you reserve names that no account owns yet. If you want the wider picture of user lookups in WordPress, my guide on checking if a user exists covers email_exists() and get_user_by() too.
What username_exists() Returns
The function lives in wp-includes/user.php and has been part of core for a very long time. It accepts one argument and returns either a user ID or false. It performs no sanitization for you, and it never writes anything — it is a read-only lookup.
| Input | Type | What it means |
|---|---|---|
$username | string | The login name to look for. Sanitize it with sanitize_user() first. |
| Return value | int|false | The user ID when a match exists, false when the name is free. |
Two details trip people up. First, a match returns an integer, never true, so a strict === true comparison always fails. Second, the lookup also falls back to user_nicename and user_email, which means passing an email address can also return an ID. Because the default MySQL collation is case-insensitive, Admin and admin are treated as the same name.
Basic Usage of username_exists()
The minimal pattern is a conditional on the sanitized input, which is exactly what you want inside an AJAX handler or a shortcode callback:
$username = sanitize_user( $_POST['username'] );
if ( username_exists( $username ) ) {
echo '<p class="error">Sorry, this username is already taken.</p>';
} else {
echo '<p class="success">Congratulations, this username is available.</p>';
}To reuse the result instead of just branching on it, keep the ID: $user_id = username_exists( $username ); gives you 0-safe access to the full record through get_user_meta() and update_user_meta() when you need to sync profile data for the account that already exists.
Add a Live AJAX Availability Check
A server round trip on every keystroke is wasteful, but a check that only fires on submit gives no feedback at all. The usual fix is a small REST route registered with register_rest_route(), called as the visitor types.
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/check-username', array(
'methods' => 'POST',
'permission_callback' => '__return_true',
'callback' => function ( WP_REST_Request $request ) {
$username = sanitize_user( $request->get_param( 'username' ) );
if ( '' === $username ) {
return new WP_Error( 'no_username', 'Enter a username.', array( 'status' => 400 ) );
}
$user_id = username_exists( $username );
return array(
'exists' => (bool) $user_id,
'message' => $user_id ? 'This username is taken.' : 'This username is available.',
);
},
) );
} );
const response = await fetch( '/wp-json/myplugin/v1/check-username', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( { username: field.value } ),
} );
const data = await response.json();
hint.textContent = data.message;Run the fetch on a debounce, roughly 300ms after the last keystroke, so you are not firing a database query per character.
[screenshot] Registration form with a live “This username is available” hint directly under the username input field.
The username_exists Filter
Since WordPress 4.9 the function ends with a filter, which is the cleanest way to change its answer without touching the call sites. The signature is apply_filters( 'username_exists', int|false $user_id, string $username ) — the matched user ID (or false) goes in, and whatever you return becomes the function’s result. That means a third-party plugin, your theme, or a site-specific snippet can all influence one shared check.
add_filter( 'username_exists', function ( $user_id, $username ) {
if ( ! $username ) {
return $user_id;
}
// Return the original value unchanged.
return $user_id;
}, 10, 2 );The second callback parameter is required — declare 2 as the accepted argument count or WordPress passes only the user ID and $username stays undefined. Always return something: a missing return leaves the filter with null, which your conditional will read as “name is free”.
Use the Filter to Reserve Usernames
The practical win is blocking names that no account owns yet — brand names, staff handles, role words people expect to exist. Because the filter only needs to report a truthy value, returning -1 is enough to mark the name as taken:
add_filter( 'username_exists', function ( $user_id, $username ) {
$reserved = array( 'admin', 'administrator', 'support', 'andriy' );
if ( in_array( strtolower( $username ), $reserved, true ) ) {
return -1; // Non-false value = "already taken".
}
return $user_id;
}, 10, 2 );Return -1, not true or a real user ID. The value flows straight back to callers, and anything that later passes it to something like get_userdata() will get a broken user object if you return a genuine ID that belongs to somebody else.
For the core registration form, hook the same check into registration_errors instead — that is where WordPress surfaces field-level validation:
add_action( 'registration_errors', function ( $errors, $sanitized_user_login ) {
if ( username_exists( $sanitized_user_login ) ) {
$errors->add( 'username_taken', 'That username is already registered.' );
}
return $errors;
}, 10, 2 );Common Mistakes to Avoid
- Comparing against
true— the function returns an integer ID. Useif ( username_exists( $name ) )or compare againstfalsewith!==. - Skipping sanitization — raw
$_POSTdata with stray whitespace or mixed case can slip past the check, so the insert fails anyway. Runsanitize_user()first, and protect the request with a nonce where it applies. - Exposing a public unauthenticated endpoint — a username lookup with
'permission_callback' => '__return_true'and no rate limit is a free account-enumeration tool. Restrict it to logged-in users or your registration form, and add throttling. - Forgetting the accepted argument count — without
10, 2the filter callback receives only one argument and reads an undefined variable. - Assuming per-site results on multisite — the query runs against the global users table, so a user who has never joined your site still counts as existing.
Tip: username_exists() does no sanitization and no rate limiting on your behalf. Treat its result as a hint for your UI, never as the security check — re-verify the value server-side at insert time.
FAQs About username_exists
Does username_exists() return true or false?
Neither, strictly. It returns the matching user ID as an integer, or false when no match is found. Use it in a truthy conditional, or test with false !== username_exists( $name ) when you need a strict check.
Is username_exists() case-sensitive?
Not in practice. The user_login lookup relies on the table’s collation, which is case-insensitive by default, so JohnDoe and johndoe resolve to the same account.
Should I sanitize the username before checking?
Yes. Pass the value through sanitize_user( $input, true ) so you compare like for like. If your form enforces strict ASCII logins, add a preg_match() guard on top, because the function itself accepts any string and will happily report false for a value your schema would reject later.
Can I block usernames without writing to the database?
Yes — that is exactly what the username_exists filter is for. Add your reserved names inside an add_filter() callback and return a truthy value for them. No placeholder accounts, no extra option row, and you can drop the whole rule with a single remove_filter().
Conclusion
username_exists() is small, but the work around it is where real projects live: sanitize first, branch on truthiness rather than === true, throttle any endpoint that exposes the lookup publicly, and remember the user ID you get back is data worth acting on rather than a boolean to discard.
Need a hand wiring username availability into your own registration flow, plugin, or membership site? I build custom WordPress and WooCommerce features like this regularly — hire an expert WordPress developer, or reach out to me directly through the application form and tell me what you are building.
