sipeed/picoclaw · error · ErrTemporary
discord send: %w
Error message
discord send: %w
What it means
sendChunk's ChannelMessageSend / ChannelMessageSendComplex (used for reply references) failed inside the goroutine; wrapped as channels.ErrTemporary so the manager retries with backoff. The raw discordgo error is discarded, so the visible error never says whether it was a 403 permission, 400 bad request, or network failure.
Source
Thrown at pkg/channels/discord/discord.go:502
err error
)
// If we have an ID, we send the message as "Reply"
if replyToID != "" {
msg, err = c.session.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{
Content: content,
Reference: &discordgo.MessageReference{
MessageID: replyToID,
ChannelID: channelID,
},
})
} else {
// Otherwise, we send a normal message
msg, err = c.session.ChannelMessageSend(channelID, content)
}
if err != nil {
done <- result{err: fmt.Errorf("discord send: %w", channels.ErrTemporary)}
return
}
done <- result{id: msg.ID}
}()
select {
case r := <-done:
return r.id, r.err
case <-sendCtx.Done():
return "", sendCtx.Err()
}
}
// appendContent safely appends content to existing text
func appendContent(content, suffix string) string {
if content == "" {
return suffix
}View on GitHub (pinned to 49183d7e8d)
Solutions
- Verify the bot can send in the target channel (permissions, channel exists, not archived)
- Confirm content length is under 2000 runes so the splitter is doing its job
- Patch the wrap to preserve the cause: fmt.Errorf("discord send: %w: %w", channels.ErrTemporary, err)
- Log err inside the goroutine before sending to done so failures are diagnosable in logs
Example fix
// before
if err != nil {
done <- result{err: fmt.Errorf("discord send: %w", channels.ErrTemporary)}
return
}
// after
if err != nil {
logger.WarnCF("discord", "send failed", map[string]any{"channel": channelID, "error": err.Error()})
done <- result{err: fmt.Errorf("discord send: %w: %w", channels.ErrTemporary, err)}
return
} Defensive patterns
Strategy: retry
Validate before calling
if len([]rune(content)) > 2000 {
content = content[:2000] // or split — channel MaxMessageLength is 2000
} Type guard
func isTemporarySend(err error) bool { return errors.Is(err, channels.ErrTemporary) } Try / catch
if _, err := ch.Send(ctx, msg); err != nil {
if errors.Is(err, channels.ErrTemporary) {
// backoff-retry once; permission/oversize failures repeat forever — inspect logs
}
return err
} Prevention
- Respect the 2000-rune limit by always going through the splitter
- Check bot permissions in the target channel after permission changes
- Patch the wrap to include the discordgo error so 403 vs 400 vs network is visible
- Log the underlying err inside the send goroutine before reporting ErrTemporary
When it happens
Trigger: Content exceeding Discord's 2000-char limit (the channel sets MaxMessageLength 2000 and normally splits first), missing SEND_MESSAGES permission, deleted/archived/gone channelID, replying to a deleted message via MessageReference, network drop within the 10s sendTimeout.
Common situations: Bot kicked from server or channel permissions tightened, very long messages bypassing the splitter, replies racing with message deletion, transient Discord/network issues.
Related errors
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/10e17c7048b75b39.
Report an issue: GitHub.