Guide

wp_mail() return value: what true and false actually mean

wp_mail() returns a boolean, but it only reports whether WordPress handed the message to the mail transport, not whether it was delivered. Here is what true and false really mean, how to catch the false case with the wp_mail_failed hook, and why a true return still needs a delivery signal.

1. Problem

You wrote $sent = wp_mail( $to, $subject, $body ); and now you are branching on $sent to decide whether the email went out. The question you are really asking, and the one that sends people to the documentation, is simple: if wp_mail() returns true, did the email actually arrive?

The short answer is no. wp_mail() returns a boolean that reports one thing only: whether WordPress successfully handed the message to the mail transport. It says nothing about whether the message was accepted by the receiving server, whether it cleared spam filtering, or whether it landed in an inbox. A true return is the beginning of the delivery story, not the end of it.

This page is the reference for what the return value means, what makes it true versus false, how to catch the false case with the wp_mail_failed hook, and why the true case still needs a delivery signal before you can trust it. If your mail is failing outright, the wp_mail not working guide covers the causes; this page is specifically about the return value.

2. Impact

Treating a true return as proof of delivery is one of the most common and most expensive mistakes in WordPress mail handling, because the code looks correct and passes every test you run.

  • Order and account emails silently vanish. A checkout writes "confirmation sent" to the order notes because wp_mail() returned true, the message is then rejected or spam-filtered downstream, and the customer never sees it. Your logs say success; the customer opens a ticket.
  • Password resets appear to work but do not. The reset flow reports the link was emailed, the user waits, nothing comes, and support has no record of a failure because there was none at the wp_mail() layer.
  • The false case is dropped on the floor. Most code ignores the return value entirely. When wp_mail() does return false, which is an unambiguous, catchable failure, nobody is listening, so a misconfigured mailer fails every send for weeks with no alert.
  • Debugging starts in the wrong place. Because the return value looks authoritative, teams spend hours reading application code when the real failure is a bounced message or a blocked sender reputation, three layers below where wp_mail() returned.

3. What the return value actually means

wp_mail() returns bool. The value is the return of the internal PHPMailer::send() call (or of whatever a mail plugin substituted for it), wrapped in a try/catch. That single boundary is the entire meaning of the value:

Return What it means What it does NOT mean
true The transport accepted the message for sending. With PHP mail() this means the sendmail binary or SMTP relay took it. With an SMTP plugin it means the relay returned a 250 acknowledgement. Not delivered. Not in the inbox. Not un-bounced. Not un-filtered. The receiving server can still reject or quarantine it after the handoff.
false The handoff itself failed: PHPMailer threw an exception, the mailer was misconfigured, every recipient was invalid at the address-parsing stage, or a plugin short-circuited the send. Not necessarily a permanent failure, and not a bounce. A bounce happens after a successful handoff and never reaches the return value.

The mental model that keeps you out of trouble: wp_mail() is a handoff receipt, like a courier scanning a parcel at pickup. The scan proves the courier took the parcel. It does not prove the parcel arrived, and a parcel returned to sender two days later never changes the pickup scan.

This is why the failure is hard to spot. The one signal your code has access to at send time, the return value, is structurally incapable of reporting the failures that actually happen most often, which are downstream: bounces, spam foldering, and reputation blocks. Every one of those is invisible to wp_mail().

4. Cause

To use the return value correctly you need to know exactly what flips it. Internally, wp_mail() builds a PHPMailer instance, applies your From, headers, and attachments, and calls $phpmailer->send() inside a try/catch.

It returns true when send() completes without throwing. With the default PHP mailer that is when mail() returns success. With an SMTP plugin that is when the relay accepts the message.

It returns false when one of these happens:

  • PHPMailer::send() throws a phpmailerException: SMTP auth failure, connection refused or timed out, TLS negotiation failure, or the relay rejecting the envelope sender.
  • Every address supplied is invalid at the parsing stage, so there is no recipient to send to.
  • A plugin returns a non-null value from the pre_wp_mail filter, short-circuiting the send. If it returns false there, wp_mail() returns false without ever touching the mailer.

On failure, before returning false, wp_mail() fires the wp_mail_failed action with a WP_Error carrying the reason and the full message data. That hook, not the return value, is where the actionable detail lives. In Logystera, both the send and the failure are captured as the wp.email signal, which feeds the wp_emails_total metric so success and failure counts are visible as a rate rather than a per-call boolean nobody reads.

5. Solution

5.1 Diagnose (logs first)

Answer two separate questions, because they have two separate answers. First: is wp_mail() returning false (a handoff failure you can catch in PHP)? Second: is it returning true but the mail is not arriving (a downstream delivery failure you cannot see from PHP)?

1. Test the return value directly with WP-CLI.

wp eval 'var_dump( wp_mail( "you@example.com", "Test", "Body" ) );'

If this prints bool(false), the handoff is broken and you have a concrete failure to chase. If it prints bool(true) and the message never arrives, the problem is downstream and no amount of reading the return value will find it.

2. Capture the reason on failure. Hook wp_mail_failed to log the WP_Error whenever the return would be false:

add_action( 'wp_mail_failed', function ( WP_Error $error ) {
    error_log( 'wp_mail failed: ' . $error->get_error_message() );
    // $error->get_error_data() holds the full to/subject/headers array.
} );

3. See the true-but-undelivered case. This is the one PHP cannot answer. The message left successfully; whether it arrived is recorded only at your mail provider (bounces, complaints, drops) or at the receiving server. You need a delivery signal from outside WordPress, which is exactly the gap the wp.email signal and wp_emails_total metric exist to close: every send is counted, so a site that normally sends 200 mails a day and suddenly sends zero, or whose failure ratio jumps, is visible even when every individual call returned true.

5.2 Root Causes

  • Returns false: broken SMTP credentials, a blocked outbound port 25/465/587, an invalid From address rejected by the relay, or an SMTP plugin that is installed but not configured.
  • Returns true, never arrives: the message bounced, was filtered to spam, or was dropped for sender-reputation or SPF/DKIM/DMARC alignment reasons. None of these touch the return value.
  • Returns true from the wrong mailer: PHP mail() succeeds locally but has terrible deliverability, so "it works" on the return value and fails in practice. Authenticated SMTP through a transactional provider is the fix.

5.3 Fix

Stop treating true as delivered. In your own code, use the return value only to detect the handoff failure, and never to confirm delivery. A clean pattern:

$sent = wp_mail( $to, $subject, $body );
if ( ! $sent ) {
    // Handoff failed. This is catchable and worth alerting on.
    // Detail is in the wp_mail_failed hook, not in $sent.
}
// A true here means "handed off", not "delivered". Do not log
// "email delivered" on the strength of $sent alone.

Route mail through authenticated SMTP to a transactional provider (Postmark, Mailgun, SES, Resend, SendGrid). This does not change what the return value means, but it moves you from PHP mail(), which has no delivery feedback at all, to a provider that exposes bounces and complaints through a webhook.

Wire the provider's delivery webhook back into whatever you use to track sends. That webhook is the only thing that can turn "handed off" into "delivered", because delivery status is created after wp_mail() has already returned.

5.4 Verify

You have used the return value correctly when two things are true. First, every false return is caught and surfaced, not swallowed, so a handoff failure raises an alert instead of failing silently. Confirm by forcing a failure (temporarily point SMTP at a dead host) and checking that your wp_mail_failed handler fires.

# Force a failure and confirm the hook fires and the send is counted
wp eval 'var_dump( wp_mail( "invalid@@address", "x", "y" ) );'  # expect bool(false)

Second, delivery is tracked somewhere other than the return value. In Logystera that is wp_emails_total, which counts sends and failures over time so the gap between "returned true" and "actually delivered" is observable as a rate, not discovered one support ticket at a time. Healthy state is a steady send count with a low, stable failure ratio, and an alert when either the count drops to zero or the failure ratio climbs.

6. How to Catch This Early

The return value is easy to read and easy to trust. The trap is that it answers a narrower question than the one you are asking.

This issue surfaces as wp_emails_total.

The reason mail problems fester is that the only per-call feedback WordPress gives you, the wp_mail() return value, cannot report the failures that matter most, and the one failure it can report is usually ignored. So a site sends mail that returns true and quietly stops arriving, and nobody knows until a customer says "I never got the email."

Counting every wp.email event as a signal changes that. Instead of a boolean checked once and forgotten, you get a send rate and a failure rate you can watch. The first day a mailer breaks, the failure ratio steps up. The first day deliverability collapses to zero, the send count flatlines. Neither is visible from the return value, and neither triggers any default WordPress alert, which is precisely why reading it from the logs and counting it as a metric is what turns a silent mail outage into something you are told about.

7. Related Silent Failures

  • wp_emails_total drops to zero: sends stop entirely, usually a broken mailer or an expired SMTP credential, invisible if every remaining call still returns true. See emails not sending.
  • wp.email failure ratio climbs: the handoff starts failing intermittently, the exact case a wp_mail_failed handler is meant to catch. Covered in the wp_mail not working guide.
  • wp_php_warnings_total from the mailer: an SMTP plugin throwing warnings on phpmailer_init can leave the return value true while corrupting headers. See silent PHP errors.
  • Registration email spikes: a burst of true-returning sends from spam registrations can get your sender reputation blocked, which then makes real mail undeliverable while still returning true. See registration spam spike.

See what's actually happening in your WordPress system

Connect your site. Logystera starts monitoring within minutes.

Copyright © 2026 Logystera. All rights reserved.