Herald
1.x
Docs/Herald/Error Handling
Open

Reading6 min
Updated31 Jul 2026
Sourcev1/concepts/errors.mdx

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

ErrorValueDescription
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

ErrorValueDescription
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

ErrorValueDescription
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

ErrorValueDescription
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

ErrorValueDescription
herald.ErrInboxNotFound"herald: in-app notification not found"No inbox notification with the given ID exists.

Preference errors

ErrorValueDescription
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

ErrorValueDescription
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)