Handling Postmark Errors

Postmark refuses some mails for good: mail to an inactive recipient (after a hard bounce, a spam complaint or a manual suppression) and requests it can't accept, such as an invalid address. Sending them again won't help. This guide shows how to log those errors without failing the delivery, and how to post an alert to Slack without the recipients' addresses.

What Mailers Do by Default

Where LogStruct is on, it adds rescue_from StandardError, with: :log_and_reraise_error to ActionMailer::Base, so every mailer already has it. A failed delivery is logged as one error line and raised again: deliver_now raises, and a deliver_later job fails. Whether the job runs again is up to your job backend and your retry_on settings. Where LogStruct is off, mailers have no default handler (see below).

The Three Handlers

A mailer can name one of these in its own rescue_from. Each writes one error line, and then:

  • log_and_ignore_error carries on as if the mail had been sent.
  • log_and_report_error reports the error once, to your error tracker or error_reporting_handler, and carries on.
  • log_and_reraise_error raises the error again (deliver_now raises, and a deliver_later job fails), and reports it where config.error_handling_modes.standard_errors reports your app's errors (see Error Handling Configuration): in Report mode, and in ReportProduction mode in production.
With the default mode, Raise, LogStruct doesn't report the errors log_and_reraise_error raises, though an error tracker that catches raised errors may report them itself.

Where LogStruct is off

Where LogStruct is off (by default in development, consoles, Rake tasks and job runners it doesn't recognize), or with enable_actionmailer off, mailers still have the three methods, so a rescue_from that names one works. Each writes <ErrorClass>: <message> to the mailer's logger, and then:

  • log_and_ignore_error ignores the error.
  • log_and_report_error reports it to Rails.error (as handled) and doesn't raise it.
  • log_and_reraise_error raises it, without reporting it.

In a process where LogStruct is off, nothing scrubs that line, and Postmark's messages include the recipient's address.

Ignoring Postmark's Refusals

Name log_and_ignore_error for Postmark's two errors in your ApplicationMailer. Rails checks a class's own handlers before the ones it inherits, so these come before LogStruct's default handler on ActionMailer::Base:

class ApplicationMailer < ActionMailer::Base
  # Postmark won't send to these addresses, and sending again won't change that.
  # Log the error and carry on as if the mail had been sent.
  rescue_from Postmark::InactiveRecipientError,
    Postmark::InvalidEmailRequestError,
    with: :log_and_ignore_error

  # Every other StandardError reaches LogStruct's default handler, log_and_reraise_error
end

Both are subclasses of Postmark::ApiInputError, which the postmark gem raises when its API answers 422. With LogStruct on, a refused mail writes its delivery line, then this error line and no delivered line, in any mode, and a deliver_later job succeeds:

{
  "src": "mailer",
  "evt": "error",
  "lvl": "error",
  "ts": "2026-10-17T05:18:13.415+00:00",
  "to": [
    "[EMAIL:0d68e00d48aadbd7]"
  ],
  "from": "[EMAIL:5e42c76e1a39d521]",
  "subject": "Your invoice is ready",
  "msg_id": "[EMAIL:0ee4c371a7685a72]",
  "mailer": "InvoiceMailer",
  "mailer_action": "invoice_ready",
  "attachments": 0,
  "error_class": "Postmark::InactiveRecipientError",
  "msg": "Postmark::InactiveRecipientError: Cannot send email to [EMAIL:0d68e00d48aadbd7]. Error message: You tried to send to recipient(s) that have been marked as inactive. Found inactive addresses: [EMAIL:0d68e00d48aadbd7]. Inactive recipients are ones that have generated a hard bounce, a spam complaint, or a manual suppression.",
  "prog": "Rails"
}

The line also has the error's backtrace (its first 20 lines by default, config.limits.max_backtrace_lines), left out here. The recipients in the message come from the error's own recipients (an InactiveRecipientError has them) or else from the mail's to, cc and bcc, and are scrubbed like any address. See ActionMailer for the other mail lines.

Slack Alerts

LogStruct doesn't send notifications, but it publishes each of its log entries (the LogStruct::Log structs) as a log.logstruct notification when it logs it, with the entry in payload[:log] and the level in payload[:level]. To post Postmark's errors to Slack, subscribe in an initializer and enqueue a job of your own:

ActiveSupport::Notifications.subscribe("log.logstruct") do |_name, _start, _finish, _id, payload|
  log = payload[:log]
  next unless log.is_a?(LogStruct::Log::ActionMailer::Error)
  next unless [Postmark::InactiveRecipientError, Postmark::InvalidEmailRequestError].include?(log.error_class)

  # This is the log entry before LogStruct filtered it: its message, to and subject
  # can hold the recipients' addresses. Send only the mailer and the error class.
  SlackAlertJob.perform_later(
    channel: "#email-alerts",
    text: "#{log.mailer_class} couldn't send a mail: #{log.error_class.name}"
  )
end
The notification carries the entry before LogStruct filters it. Its message, to and subject can hold the recipients' addresses as they are, so send only fields like mailer_class and error_class.
  • The subscriber runs inside the logger call, after the line is queued to be written. If it raises, the line is still written and the error is handled like your app's other errors (the standard_errors mode, which raises it from the logger call by default), so keep it to enqueueing a job.
  • It runs only where LogStruct handles mail errors: in processes where LogStruct is on, with enable_actionmailer on (the default). For deliver_later, that's the job process.
  • A refused mail publishes one LogStruct::Log::ActionMailer::Error, so it sends one alert.

The Error Entry

A subscriber gets a LogStruct::Log::ActionMailer::Error. Its attributes, and their keys in the line:

AttributeKey in the lineValue
error_classerror_classThe exception class (a Class; its name in the line)
mailer_classmailerThe mailer class's name
mailer_actionmailer_actionThe mailer method
messagemsgThe error message, with the recipients
to, from, subjectto, from, subjectThe mail's addresses and subject
message_idmsg_idThe mail's Message-ID
attachment_countattachmentsHow many attachments the mail has
backtracebacktraceThe error's backtrace
additional_data(top level)IDs from actionmailer_id_mapping, such as account_id

An error raised before the mailer ran, such as a deliver_later job whose record was deleted before the job ran, is logged by the mailer class, with only mailer_class, error_class, message and backtrace.