Billionmail/BillionMail · error
failed to connect: %w
Error message
failed to connect: %w
What it means
Send() found the sender disconnected (connected==false or client==nil) and attempted an on-demand Connect(); this error wraps that Connect failure. The wrapped error is one of the connect-time errors (TLS dial, SMTP dial, STARTTLS, or auth), so diagnose by unwrapping. The mutex is unlocked/relocked around the blocking Connect call so it is not held during network I/O.
Source
Thrown at core/internal/service/mail_service/sending.go:320
return fmt.Sprintf("<%d.%s@%s>", timestampMillis, randomID, domainPart)
}
// Send sends an email to specified recipients
func (e *EmailSender) Send(message Message, recipients []string) error {
if len(recipients) == 0 {
return fmt.Errorf("no recipients specified")
}
e.mutex.Lock()
defer e.mutex.Unlock()
// Make sure we have a connection
if !e.connected || e.client == nil {
e.mutex.Unlock()
if err := e.Connect(); err != nil {
e.mutex.Lock()
return fmt.Errorf("failed to connect: %w", err)
}
e.mutex.Lock()
}
// Try to send the message, with reconnect on failure
err := e.doSend(message, recipients)
if err != nil {
// Connection might be stale, try to reconnect once
g.Log().Debug(context.Background(), "SMTP send failed, attempting reconnection")
// Clean up
e.client.Close()
e.connected = false
e.client = nil
// Try to reconnect
e.mutex.Unlock()
if err := e.Connect(); err != nil {View on GitHub (pinned to fc36c76c05)
Solutions
- Unwrap the error chain (errors.Unwrap / errors.As) to see whether it was dial, TLS, or auth, and fix that specific root cause.
- Call Connect() explicitly at sender creation/startup so failures surface early rather than at first Send.
- Add retry with backoff around reconnects for transient network errors before surfacing to the caller.
- Verify SMTP host/port/credentials in the environment the sender was constructed from.
Example fix
// before
if err := e.Connect(); err != nil {
e.mutex.Lock()
return fmt.Errorf("failed to connect: %w", err)
}
// after
if err := e.Connect(); err != nil {
e.mutex.Lock()
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
time.Sleep(2 * time.Second)
if retryErr := e.Connect(); retryErr == nil {
return nil // continue send flow
}
}
return fmt.Errorf("failed to connect: %w", err)
} Defensive patterns
Strategy: try-catch
Validate before calling
if err := sender.Connect(); err != nil {
return fmt.Errorf("SMTP pre-flight connect failed: %w", err)
}
// sender is now warm; Send() will skip its lazy-connect path Type guard
func isConnectErr(err error) bool {
return strings.Contains(err.Error(), "failed to connect")
} Try / catch
if err := sender.Send(msg, rcpts); err != nil {
if isConnectErr(err) {
var dnsErr *net.DNSError
switch {
case errors.As(err, &dnsErr):
return fmt.Errorf("bad SMTP host config: %w", err)
case strings.Contains(err.Error(), "SMTP auth"):
return ErrBadCredentials
default:
time.Sleep(2 * time.Second) // transient: retry once
return sender.Send(msg, rcpts)
}
}
return err
} Prevention
- Connect explicitly at sender creation so config errors surface early.
- Unwrap the error chain — the root cause is always a dial/TLS/auth error from Connect.
- Retry only transient (timeout/unreachable) errors; auth failures must not be retried.
When it happens
Trigger: First Send on a never-connected sender, or Send after Disconnect/server idle timeout, where Connect() fails — bad host/port, network down, or rejected credentials.
Common situations: Long-lived sender left idle until the server dropped the connection, then reconnect fails because credentials expired; misconfigured host discovered only at first send; transient network outage at send time; container lost DNS/egress.
Related errors
- reconnect failed: %w
- failed to connect to SMTP server: %v
- redis connection test failed after 10 attempts
- SMTP server is required
- SMTP port must be between 1 and 65535
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/21983108ba1b8202.
Report an issue: GitHub.