Skip to content

Mail -- Error Reference & Troubleshooting

Complete error code reference and troubleshooting guide for the Mail component.

Error reference

All errors are created via getError(). Only errors that pass statusCode/messageCode carry an ApplicationError identity with those fields -- the rows marked -- throw a plain error with just a message. Read an error's code at error.normalized.code; there is no flat error.messageCode.

MailService errors

ConditionStatusError codeMessage
to is missing or an empty array400core.mail.invalid_recipientRecipient email address is required
subject is falsy400core.mail.invalid_configurationEmail subject is required
Both text and html are falsy400core.mail.invalid_configurationEmail must have either text or html content
Transport throws during send()500core.mail.send_failedFailed to send email: <error>
Batch operation fails500core.mail.batch_send_failedFailed to send batch emails: <error>
Template engine not configured for sendTemplate()500core.mail.invalid_configurationTemplate engine not configured
Transport throws during verify()500core.mail.verification_failedMail transport verification failed: <error>

NOTE

"Transport throws during send()/verify()" only fires for a custom transport. The built-in NodemailerTransportHelper and MailgunTransportHelper never throw from send() or verify() -- they catch internally and return { success: false, error } (or false for verify()). A 400 validation error raised by validateMessage() is re-thrown unchanged, not wrapped as SEND_FAILED.

MailComponent errors

ConditionStatusError codeMessage
MAIL_OPTIONS not bound before component(MailComponent)----Mail options not configured

MailTransportProvider errors

ConditionStatusError codeMessage
Unsupported provider string500core.mail.invalid_configurationUnsupported mail provider: <provider>
Nodemailer options fail the type guard500core.mail.invalid_configurationInvalid Nodemailer configuration
Mailgun options fail the type guard500core.mail.invalid_configurationInvalid Mailgun configuration
Custom options fail the type guard500core.mail.invalid_configurationInvalid custom mail provider configuration
Custom config missing send/verify500core.mail.invalid_configurationCustom mail provider must implement IMailTransport interface. Missing methods: <methods>

MailgunTransportHelper errors

ConditionStatusError codeMessage
config is missing username, key, or domain500core.mail.invalid_configurationInvalid Mailgun configuration | Missing required keys: <keys>

This check runs on configure(), one layer deeper than MailTransportProvider's type guard -- a config object that passes the provider's guard (has a config property) can still fail this one if it is missing the specific keys Mailgun's client needs.

MailQueueExecutorProvider errors

ConditionStatusError codeMessage
config.internalQueue missing for internal-queue type----Internal queue configuration is missing
config.bullmq missing for bullmq type----BullMQ configuration is missing
Unknown executor type----Unknown mail queue executor type: <type>

TemplateEngineService errors

ConditionStatusError codeMessage
Neither templateName nor templateData provided----Either templateName or templateData must be provided
Template name not found in registry404core.mail.template_not_foundTemplate not found: <name>
Missing template data keys (with requireValidate: true)400core.mail.invalid_configurationMissing template data for keys: <keys>

Queue executor errors

ConditionExecutorMessage
Processor not set before enqueueDirect, Internal Queue -- alwaysProcessor not set. Call setProcessor() first.
Processor not set before enqueueBullMQ -- only outside 'queue-only' modeProcessor not set. Call setProcessor() first.
Processor not set before adding a workerBullMQProcessor not set. Call setProcessor() first.
Enqueue attempted in worker-only modeBullMQCannot enqueue jobs in worker-only mode. Set mode to "queue-only" or "both".
Queue helper unexpectedly nullBullMQQueue helper not initialized. This should not happen in queue-enabled mode.

Troubleshooting

"Mail options not configured"

  • Cause. MailKeys.MAIL_OPTIONS was not bound before MailComponent was registered. binding() checks isBound() and throws immediately.
  • Fix. Bind the options before calling this.component(MailComponent):
typescript
this.bind({ key: MailKeys.MAIL_OPTIONS }).toValue({
  provider: MailProviders.NODEMAILER,
  from: 'noreply@example.com',
  config: { host: 'smtp.example.com', port: 587, secure: false, auth: { user: '...', pass: '...' } },
});

this.component(MailComponent);

"Invalid Mailgun configuration | Missing required keys: ..."

  • Cause. The Mailgun config object is missing username, key, or domain. A common mistake is naming the field apiKey instead of key.
  • Fix. Use the exact field names mailgun.js expects:
typescript
config: { username: 'api', key: process.env.MAILGUN_API_KEY, domain: 'mg.example.com' }

core.mail.template_not_found from sendTemplate()

  • Cause. The template name was never registered via templateEngine.registerTemplate() -- often because a database-backed sync runs after the first sendTemplate() call, not before it.
  • Fix. Register the template first:
typescript
this.templateEngine.registerTemplate({
  name: 'welcome-email',
  content: '<h1>Welcome {{userName}}</h1>',
});

Emails silently fail with success: false

  • Cause. The transport connection is misconfigured (wrong credentials, blocked port, expired OAuth2 token). MailService.send() never throws for a built-in transport's connection failure -- it returns { success: false, error }.
  • Fix. Check result.error for the underlying transport error, and verify the connection at startup:
typescript
const isConnected = await this.mailService.verify();
if (!isConnected) {
  this.logger.error('Mail transport verification failed');
}

BullMQ executor is not processing jobs

  • Cause. Either mode is 'queue-only' (enqueues but never starts a worker), or the Redis connection is unreachable.
  • Fix. Run mode: 'both' or 'worker-only' on the instance meant to process jobs, and confirm Redis connectivity:
typescript
{
  type: 'bullmq',
  bullmq: {
    redis: { host: 'localhost', port: 6379 /* ... */ },
    queue: { identifier: 'mail-queue', name: 'mail-queue' },
    mode: 'both', // must be 'both' or 'worker-only' to process
  },
}

Template placeholders show up literally in the output

  • Cause. The data object passed to render()/sendTemplate() is missing a {{key}} the template uses. Without requireValidate: true, the engine preserves the original placeholder text -- it does not replace a missing value with an empty string.
  • Fix. Check what is missing before rendering, or fail loudly instead:
typescript
const validation = this.templateEngine.validateTemplateData({ template, data });
if (!validation.isValid) {
  console.error('Missing keys:', validation.missingKeys);
}
typescript
const html = this.templateEngine.render({
  templateName: 'welcome-email',
  data,
  requireValidate: true, // throws instead of leaving placeholders in the output
});

"Processor not set. Call setProcessor() first."

  • Cause. enqueueVerificationEmail() was called before setProcessor(). Direct and Internal Queue always require this; BullMQ requires it too, except in 'queue-only' mode, where enqueueing does not need a processor.
  • Fix. Register a processor before enqueuing:
typescript
executor.setProcessor(async (email: string) => {
  // Your email-sending logic, typically wrapping mailService.send()
  return { success: true, message: 'Verification email sent', expiresInMinutes: 10 };
});

"Cannot enqueue jobs in worker-only mode"

  • Cause. The BullMQ executor is running with mode: 'worker-only', which never creates a queue and therefore cannot accept new jobs.
  • Fix. Use 'both' or 'queue-only' on any instance that needs to enqueue. Reserve 'worker-only' for dedicated worker processes that only consume.

Startup logs and credentials

MailComponent.createAndBindInstances() logs only mailOptions.provider and queueExecutorConfig.type at info level -- by design, it never logs the full config object, so SMTP passwords, OAuth2 secrets, Mailgun API keys, and Redis passwords are never written to the log by the component itself.

WARNING

That guarantee is scoped to MailComponent's own logging. If your wrapper component (or any other code) logs the TMailOptions/IMailQueueExecutorConfig object directly -- for example logger.info('%j', mailOptions) while debugging -- you reintroduce the leak yourself. Log individual safe fields instead of the whole object.

See also

  • Overview -- quick start, imports, common configuration tasks
  • Usage & Examples -- sending emails, templates, queue executors, verification generators
  • API Reference -- architecture, binding keys, interfaces, and internals