Sending emails with nodemailer on Vercel

By

Learn how to send emails with nodemailer on Vercel, where the callback version of sendMail silently fails in serverless and you must await the promise instead.

~~~

To send emails with nodemailer on Vercel, you must await the promise returned by sendMail(). The callback version silently fails in a serverless environment. I learned this the hard way, so let me save you the debugging session.

I couldn’t figure out why nodemailer didn’t work on Vercel, then (tldr) I found out I needed await and not a callback.

Here’s the code I had:

nodemailer
  .createTransport({
    host: 'smtpserver.com',
    port: 465,
    secure: true,
    auth: {
      user: import.meta.env.USER,
      pass: import.meta.env.PASS,
    },
  })
  .sendMail(
    {
      from: 'me@me.com',
      to: email,
      subject,
      html,
    },
    function (err, info) {
      console.log(info)
      if (err) {
        console.log(err)
      } else {
        console.log('sent email')
      }
    }
  )

This worked locally.

But when pushed to Vercel and it ran in a serverless function environment, the email was never sent.

Also, I never got the “sent email” message in the logs.

Nor any error.

Why the callback version fails

On your machine, the Node.js process stays alive after the response is sent. The SMTP conversation with the mail server finishes in the background, and the callback fires.

A serverless function is different. As soon as your handler returns the response, the platform freezes or kills the execution. Any work still pending at that point never completes.

The email send is exactly that kind of pending work. The callback never runs, so you get no log line and no error. The function just ends first.

The fix

sendMail() returns a promise when you call it without a callback. Awaiting that promise keeps the handler alive until the email is actually handed off to the SMTP server:

try {
  await nodemailer
    .createTransport({
      host: 'smtpserver.com',
      port: 465,
      secure: true,
      auth: {
        user: import.meta.env.FASTMAIL_EMAIL,
        pass: import.meta.env.FASTMAIL_APP_SPECIFIC_PASSWORD,
      },
    })
    .sendMail({
      from: 'me@me.com',
      to: email,
      subject,
      html,
    })
  console.log('Email sent to ' + email)
} catch (e) {
  console.error(e)
}

The try/catch replaces the error handling we had in the callback. If the SMTP server rejects the message, you now see the actual error in the Vercel logs.

The general rule

This is not a nodemailer quirk. Any fire-and-forget async work in a serverless function has the same problem: a database write, a fetch to an analytics service, anything you start and don’t await.

Await everything before returning the response. If it’s not awaited, assume it won’t happen.

Tagged: Services · All topics
~~~

Related posts about services: