Error Handling
Sentinel errors and error patterns used across Herald.
Herald defines sentinel errors in the root herald package and the driver package. Use errors.Is to check for specific error conditions in your application code.
Using errors.Is
import (
"errors"
"github.com/xraph/herald"
)
result, err := h.Send(ctx, req)
if errors.Is(err, herald.ErrTemplateNotFound) {
// template does not exist for this app + channel
}All Herald services and store backends return these sentinel errors. Store implementations wrap them so that errors.Is works regardless of which backend (PostgreSQL, SQLite, MongoDB, or in-memory) is in use.
Sentinel errors by category
Store and lifecycle errors
| Error | Value | Description |
|---|---|---|
herald.ErrNoStore | "herald: store is required" | No store backend was provided to herald.New. |
herald.ErrStoreClosed | "herald: store is closed" | A store operation was attempted after the store was closed. |
herald.ErrMigrationFailed | "herald: migration failed" | A database schema migration failed. |
Provider errors
| Error | Value | Description |
|---|---|---|
herald.ErrProviderNotFound | "herald: provider not found" | No provider with the given ID exists. |
herald.ErrProviderDisabled | "herald: provider is disabled" | The resolved provider has Enabled: false. |
herald.ErrNoProviderConfigured | "herald: no provider configured for channel" | No enabled provider exists for the requested channel after scope resolution. |
herald.ErrDriverNotFound | "herald: driver not found" | The driver named by the provider is not registered. |
The driver package also defines its own not-found error:
driver.ErrDriverNotFound // "herald: driver not found"Template errors
| Error | Value | Description |
|---|---|---|
herald.ErrTemplateNotFound | "herald: template not found" | No template with the given slug + channel exists for this app. |
herald.ErrTemplateDisabled | "herald: template is disabled" | The template has Enabled: false. |
herald.ErrNoVersionForLocale | "herald: no template version for locale" | No active template version matches the requested locale. |
herald.ErrTemplateRenderFailed | "herald: template rendering failed" | Template rendering failed (syntax error, execution error). |
herald.ErrMissingRequiredVariable | "herald: missing required template variable" | A required template variable was not provided in the Data map. |
herald.ErrDuplicateSlug | "herald: duplicate template slug" | A template with the same slug + channel + app already exists. |
herald.ErrDuplicateLocale | "herald: duplicate locale version" | A template version for the same locale already exists on this template. |
Message and delivery errors
| Error | Value | Description |
|---|---|---|
herald.ErrMessageNotFound | "herald: message not found" | No message with the given ID exists. |
herald.ErrSendFailed | "herald: send failed" | Notification delivery failed at the driver level. |
herald.ErrInvalidChannel | "herald: invalid channel type" | An unsupported channel type was specified. |
Inbox errors
| Error | Value | Description |
|---|---|---|
herald.ErrInboxNotFound | "herald: in-app notification not found" | No inbox notification with the given ID exists. |
Preference errors
| Error | Value | Description |
|---|---|---|
herald.ErrPreferenceNotFound | "herald: user preference not found" | No preference record exists for the given app + user. |
herald.ErrOptedOut | "herald: user opted out" | The user has opted out of this notification type on the requested channel. |
Scoped config errors
| Error | Value | Description |
|---|---|---|
herald.ErrScopedConfigNotFound | "herald: scoped config not found" | No scoped configuration exists for the given app + scope type + scope ID. |
Error handling patterns
Check for specific errors
result, err := h.Send(ctx, &herald.SendRequest{
AppID: "myapp",
Channel: "email",
Template: "auth.welcome",
To: []string{"user@example.com"},
Data: map[string]any{"name": "Alice"},
})
switch {
case errors.Is(err, herald.ErrTemplateNotFound):
// template does not exist -- check slug and channel
case errors.Is(err, herald.ErrNoProviderConfigured):
// no email provider configured for this app
case errors.Is(err, herald.ErrDriverNotFound):
// the provider's driver is not registered
case err != nil:
// unexpected error
default:
// success -- check result.Status
}Template resolution failures
When sending with a template, Herald resolves the template by slug and channel, then renders the appropriate locale version. Multiple errors can occur in this chain:
result, err := h.Send(ctx, req)
switch {
case errors.Is(err, herald.ErrTemplateNotFound):
// slug + channel combination not found for this app
case errors.Is(err, herald.ErrTemplateDisabled):
// template exists but is disabled
case errors.Is(err, herald.ErrNoVersionForLocale):
// no active version for the requested locale
case errors.Is(err, herald.ErrMissingRequiredVariable):
// a required variable was not provided in Data
case errors.Is(err, herald.ErrTemplateRenderFailed):
// Go template execution error
}Provider resolution failures
The scope resolver walks the chain (user -> org -> app -> fallback) looking for an enabled provider. If none is found:
if errors.Is(err, herald.ErrNoProviderConfigured) {
// No enabled provider for this channel.
// Either create a provider or configure a scoped config.
}Preference opt-out handling
Herald checks user preferences automatically during Send. If the user has opted out, the send returns a result (not an error) with the opt-out reason:
result, err := h.Send(ctx, req)
if err == nil && result.Error == "user opted out" {
// User opted out of this notification type on this channel.
// result.Status is StatusSent (silent skip).
}Store error mapping
All store backends map database-level errors to Herald sentinel errors. For example, a PostgreSQL unique constraint violation on the template slug is mapped to ErrDuplicateSlug, and a "no rows" result is mapped to the appropriate Err*NotFound error.
err := store.CreateTemplate(ctx, tmpl)
if errors.Is(err, herald.ErrDuplicateSlug) {
// A template with this slug + channel already exists for this app.
}Wrapped errors
Service methods may wrap sentinel errors with additional context using fmt.Errorf and %w. Always use errors.Is rather than string comparison:
// Works correctly even when the error is wrapped with context.
if errors.Is(err, herald.ErrTemplateNotFound) {
// ...
}For example, the Send method wraps template errors:
// Internally: fmt.Errorf("%w: %v", ErrTemplateNotFound, storeErr)