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.Is01
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 category02
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 patterns03
Check for specific errors
result, err := h.Send(ctx, &herald.SendRequest{
AppID: "myapp",
Channel: "email",
Template: "auth.welcome",
To: []string{"[email protected]"},
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 errors04
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)