TryGhost/Ghost · error · EmailFailedError

post.email.error

Error message

post.email.error

What it means

Thrown as an EmailFailedError (ghost-admin/app/errors/email-failed-error.js) when a published post's email record has status 'failed'. The publish-management retry loop reloads the post, checks post.email.status, and surfaces the underlying post.email.error to the editor. It represents a server-side email delivery failure, not a client-side validation problem.

Source

Thrown at apps/ember-admin/app/components/editor/publish-management.js:303

        let pollTimeout = 0;
        if (post.email && post.email.status !== 'submitted') {
            while (pollTimeout < CONFIRM_EMAIL_MAX_POLL_LENGTH) {
                yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
                pollTimeout += CONFIRM_EMAIL_POLL_LENGTH;

                yield post.reload();

                if (!post.isSent && !post.isPublished) {
                    // A post that is not published doesn't try to send or retry an email
                    break;
                }

                if (post.email.status === 'submitted') {
                    break;
                }
                if (post.email.status === 'failed') {
                    throw new EmailFailedError(post.email.error);
                }
            }
        }

        return true;
    }

    @task
    *revertToDraftTask() {
        try {
            yield this.publishTask.perform({taskName: 'revertToDraftTask'});

            const postType = capitalize(this.args.post.displayName);
            this.notifications.showNotification(`${postType} reverted to a draft.`, {type: 'success'});

            return true;
        } catch (e) {
            this.notifications.showAPIError(e);

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. In Ghost admin go to Settings > Email and verify the from-address and mail service connection, then re-test delivery.
  2. Open the post in the editor and use the retry-email action; if it fails again, inspect the exact post.email.error payload from the API response (Network tab on /admin/posts/<id>/?include=email) for the provider's reason.
  3. If using a custom SMTP/Mailgun setup, confirm the configured domain's SPF/DKIM/MX records and that the API key has 'send' scope.
  4. For rate-limit failures, wait and reduce batch size or schedule the send; check the email service provider dashboard for throttling.

Example fix

// before: status==='failed' surfaces raw provider error
throw new EmailFailedError(post.email.error);

// caller (publish-flow.js:103) already discriminates by name:
if (e?.name === 'EmailFailedError') { /* show modal with retry */ }
Defensive patterns

Strategy: try-catch

Type guard

// distinguish the email-failed signal from other errors
function isEmailFailedError(e) {
    return e?.name === 'EmailFailedError' || /email/i.test(e?.message || '');
}

Try / catch

try {
    yield this.publishTask.perform({taskName: 'sendEmailTask'});
} catch (e) {
    if (e?.name === 'EmailFailedError') {
        // show the email-failure modal with retry option (see publish-flow.js:103)
        this.showEmailFailureModal(e);
    } else {
        throw e; // rethrow non-email errors
    }
}

Prevention

When it happens

Trigger: After triggering send/retry of a post email via publish-management's publishTask, the loop polls post.reload(); the returned post is isSent or isPublished AND post.email.status === 'failed'. The error message is whatever the API returned in post.email.error (e.g. Mailgun timeout, invalid recipient domain, rate limit).

Common situations: Mailgun/SES provider misconfiguration in Ghost settings, DNS/MX issues on recipient domains, hitting provider rate limits during large broadcasts, scheduled post going out while email service credentials expired, or the post was retried after a partial failure and the second attempt also failed.

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/893a6de8557c71cc. Report an issue: GitHub.