langfuse/langfuse · error · Error

Email(s) (${failed.join(", ")}) could not be sent

Error message

Email(s) (${failed.join(", ")}) could not be sent

What it means

After attempting to send a password-reset email, the sender checks nodemailer's result for rejected or pending recipients; if any recipient failed, it throws listing the failed addresses. The code notes the SES transport omits these fields, so it guards against undefined before reading them — any non-empty rejection or pending list fails the request.

Source

Thrown at packages/shared/src/server/services/email/passwordReset/sendResetPasswordVerificationRequest.tsx:117

  const textBody = isSetupMode
    ? `Welcome to Langfuse! Use the following code to verify your email: ${token}\n\nThis code will expire in 3 minutes. If you did not request this, you can ignore this email.`
    : `Use the following code to reset your Langfuse password: ${token}\n\nThis code will expire in 3 minutes. If you did not request a reset, you can ignore this email.`;

  const result = await transport.sendMail({
    to: identifier,
    from: provider.from,
    subject,
    text: textBody,
    html: htmlTemplate,
  });
  // nodemailer's SES transport omits `rejected`/`pending` from SentMessageInfo,
  // so guard against undefined before reading them.
  const failed = [...(result.rejected ?? []), ...(result.pending ?? [])].filter(
    Boolean,
  );
  if (failed.length) {
    throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
  }
}

export default ResetPasswordTemplate;

View on GitHub (pinned to 59d92c7cf3)

Solutions

  1. Verify the recipient email address is valid and deliverable before sending (format + MX check)
  2. Check SMTP/SES provider logs and dashboards for the rejection reason (bounce, greylist, policy)
  3. If greylisting/pending is transient, retry after a delay or let the user resend
  4. Confirm transport configuration (host, port, auth) points at the intended relay
Defensive patterns

Strategy: try-catch

Validate before calling

const emailSchema = z.string().email();
if (!emailSchema.safeParse(user.email).success) {
  return { ok: false, reason: "invalid-email" };
}

Type guard

const hasRejectedRecipients = (
  info: SentMessageInfo
): info is SentMessageInfo & { rejected: string[] } =>
  Array.isArray(info.rejected) && info.rejected.length > 0;

Try / catch

try {
  await sendResetPasswordVerificationRequest({ email });
} catch (err) {
  if (err instanceof Error && /could not be sent/.test(err.message)) {
    res.status(422).json({ error: "We could not deliver a reset email to that address. Verify it and try again." });
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling sendResetPasswordVerificationRequest where the SMTP/transport server rejects the recipient address (e.g. 550 unknown user, invalid domain) or leaves it pending; result.rejected or result.pending then contains the email address.

Common situations: User signs up with a typo'd or nonexistent email then requests a password reset; SMTP relay greylists/defers the message (pending); mailbox full or domain MX missing; transport misconfiguration causing partial failures.


AI-assisted analysis of langfuse/langfuse@59d92c7cf3 (2026-08-27). Data as JSON: /api/errors/c54d5038d2ba0e86. Report an issue: GitHub.