Today I learned

Pitfalls of Rails mailer conventions

Typical Rails convention will tell you to send email like using the ActiveMailer API using #deliver_later. This is not a very good idea, and let me tell you why.

NotificationMailer.notification(@user, @post).deliver_later

This kind of mailer would typically be implemented like so:

class NotificationMailer < ApplicationMailer
  def notification(user, post)
    @user = user
    @post = post

    mail to: @user.email,
         subject: "You've got a notification"
  end
end
Next: Let's look at how this can cause problems.

Race condition!

If you follow Rails best practices, you'd be using #deliver_later to send email in the background. This is great for performance, but that means you have to start thinking asynchronously. The code above is written in a very synchronous style and will fail in certain conditions. Consider this scenario:

  • A notification is triggered for user Joe, who is [email protected], by creating a new Post.
  • A background job is created.
  • Before an email could be sent, the Post is deleted.
  • The mailer now fails because @post is nil.
Next: So how can we fix this?

Think asynchronously

Instead of passing Rails models or record ID's, pass a fully-loaded plain hash of everything the mailer needs to send the email. Consider something like this instead:

NotificationMailer.notification(
  user: {
    email: @user.email
  },
  post: {
    id: @post.id,
    title: @post.title,
    url: post_url(@post)
  }
).deliver_later

The mailer will now not need to hit the database to fetch any data, eliminating any race conditions. Even if the post is modified or deleted later on, the email will still send just fine.

You have just read Pitfalls of Rails mailer conventions, written on June 06, 2017. This is Today I Learned, a collection of random tidbits I've learned through my day-to-day web development work. I'm Rico Sta. Cruz, @rstacruz on GitHub (and Twitter!).

← More articles