AnnouncementsIntroducing Arkoren

    Menu

    Error Handling

    Classify scheduled run outcomes, observe failures, and separate cooperative cancellation from driver recovery.

    What's on this page
    1. Distinguish successful, deliberately skipped, and failed handler outcomes.
    2. Apply cooperative timeouts and observe failures through logs and configured events.
    3. Recognize driver recovery differences and registration errors before execution.
    On this page

    Scheduled work has no request caller waiting to recover a failure. The scheduler records what your handler returned; the driver decides whether the engine retries or moves on. Keep those responsibilities separate when designing alerts and repeatable business operations.

    Run Outcomes

    Every executed entry or task produces one scheduler outcome:

    Outcome Handler Result Driver Receives
    ok Returns nil. Nil.
    skipped Returns an error matching schedule.ErrSkipped. Nil.
    failed Returns another error or panics. An error.

    An overlap tick suppressed by a driver never calls the handler, so it is not the same as a handler returning ErrSkipped. Likewise, reaching a deadline does not independently classify the run: a handler returning nil after cancellation still reports ok.

    Skipping Work

    If a stock refresh has nothing new to process, return ErrSkipped rather than an ordinary failure. With application functions stockChanged and refreshStock, import context and arkoren.dev/core/service/schedule. The first function has type func(context.Context) (bool, error); the second has type func(context.Context) error:

    func refreshIfNeeded(ctx context.Context) error {
    	changed, err := stockChanged(ctx)
    	if err != nil {
    		return err
    	}
    	if !changed {
    		return schedule.ErrSkipped
    	}
    
    	return refreshStock(ctx)
    }

    The scheduler logs a skip, labels telemetry skipped, and returns nil to the driver. With an event bus configured, it emits schedule.run.skipped, not schedule.run.failed. Wrapped skips work because classification uses errors.Is.

    You can also skip when application coordination denies a lock, but a skip does not itself establish exclusive execution. Choose appropriate locking and fencing if concurrent effects would violate correctness.

    Timeout Boundaries

    To bound a recurring refresh, configure a positive Timeout during entry construction. Import time:

    refresh := schedule.NewEntry("refresh-stock", refreshIfNeeded, func(entry *schedule.EntryBuilder) {
    	entry.When(schedule.UTC().Every(5 * time.Minute))
    	entry.Timeout(4 * time.Minute)
    })

    This cancels the handler context after four minutes. Pass it into database and network calls, or explicitly check it during loops. If the function returns ctx.Err(), the run is failed; returning nil instead reports success even if the deadline passed. The default zero timeout imposes no facade bound, and negative values are declaration errors.

    No timeout forcibly terminates Go code. Driver-native activity or handler timeouts can record an engine timeout while application code continues. Memory detaches admitted callbacks from runner cancellation and drains without a driver timeout; an application’s shutdown wait budget is not a callback lifetime bound. Bound task handlers themselves as shown in Deferred Tasks, and keep their dependencies alive until execution ends. See Drivers for native timeout and shutdown policies.

    Failure Events

    For an ordinary error, the scheduler logs the failure and duration, marks the run span as failed, and returns an error wrapped as run <name>: .... Execution panics are recovered into errors carrying the panic value and stack, with schedule.panic as telemetry’s error type. Invalid task JSON fails during decoding before the handler runs.

    To notify an operator without embedding alert delivery in every handler, configure events when constructing the scheduler. With an existing application app and selected driver:

    scheduler := schedule.New(driver, func(clock *schedule.Builder) {
    	clock.Events(app.Events())
    })

    Before execution, register a typed listener. Here, notifyOnCall is your application’s func(context.Context, string) error notification function:

    _, err := app.Events().Listen(schedule.RunFailed, func(ctx context.Context, failure schedule.RunFailedEvent) error {
    	return notifyOnCall(ctx, failure.Name)
    })

    The event contains the name, entry/task kind, duration, and original error. It also fires for RunNow failures. Listener errors and panics are logged by the event bus and do not change the run result; remaining listeners are skipped after that failure. Alert delivery is not a transaction with execution.

    Automatic telemetry excludes error text, payloads, and job IDs, but failure logs and events carry the error. Do not put secrets in errors. See Built-in Events for payload details and scheduler telemetry for instrumentation.

    Driver Recovery

    A failed callback does not, by itself, end Scheduler.Run. The driver receives the dispatch error and applies its execution policy:

    • Memory logs the failure without retrying it; the next calendar tick is independent.
    • Temporal configures one activity attempt per firing; a failed run does not prevent the next schedule firing.
    • Restate makes ordinary application errors terminal. Missing dispatch and any error matching schedule.ErrUnknownName remain retryable, including an application error wrapping that sentinel.

    Do not add submission retries as a substitute for understanding those policies. The facade does not automatically retry At or After. A repeated submission creates another job, and an uncertain transport result can require reconciliation.

    Make effects idempotent even when failed handlers are not retried: repeated delivery and interrupted execution can still repeat work. Overlap policy is not deduplication and does not cover manual RunNow; see Overlap Policy.

    Runner startup failures are separate. Temporal returns reconciliation errors from Run, allowing the application runner supervisor to stop the application. Restate instead retries initial publication every second until ingress accepts it or the context is canceled. An unreachable server can therefore leave publication waiting rather than immediately failing the runner.

    Registration Errors

    Handle returned registration and submission errors using errors.Is rather than matching their text:

    Error Meaning
    schedule.ErrInvalidName An entry or task name violates the portable naming rule.
    schedule.ErrInvalidEntry A required spec or handler is missing, timing is invalid, a timeout is negative, or At received zero time.
    schedule.ErrDuplicateName An entry or task already uses that name.
    schedule.ErrUnknownName At or After has no registered handler, or RunNow cannot find an entry. Application errors can also wrap this identity.
    schedule.ErrRunning Registration followed Run, or the facade was run a second time.
    schedule.ErrUnsupported The driver cannot perform the operation, such as an engine entry using the Local zone.

    Add validates its entire batch before changing registration. Entry definitions retain their first declaration error; later callbacks cannot repair it. When is required even if construction callbacks are omitted or empty.

    Construction callbacks run once in order, including for invalid declarations. A nil execution function is a returned declaration error, but a supplied nil construction callback or callback panic propagates. Runtime handler panic recovery does not apply during construction.

    For every function and option, see the schedule API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive