Guides
Callbacks and errors
What each hook is for and exactly when it fires, and what happens when a job fails.
On this page
When each hook fires
| Hook | Fires | Awaited |
|---|---|---|
callback |
On every tick. This is the job itself. | Yes |
onTick |
After callback finishes successfully |
Yes |
onError |
After callback throws or rejects |
Yes |
onComplete |
When the job is destroyed | No |
baker.add({
name: "import-feed",
cron: "@hourly",
callback: async () => {
const started = Date.now();
try {
await importFeed();
} finally {
console.log(`import-feed took ${Date.now() - started} ms`);
}
},
onTick: () => console.log("Import succeeded"),
onError: (error) => console.error("Import failed:", error.message),
onComplete: () => console.log("Job removed"),
});What a failure does
When a callback throws or returns a rejected promise:
- The error is caught, so it can’t crash your process.
- The run is recorded as failed in the job’s metrics and history, and
lastErroris updated. - Exactly one error handler is called.
- The job stays
runningand tries again at its next tick.
Anything thrown that isn’t an Error is wrapped with new Error(String(value)) before it reaches your handler.
Which handler is called
The most specific handler wins, and only that one runs.
- The job’s own
onError. - The baker’s
onError, which also receives the job name. - If neither exists,
logger.warn().
const baker = Baker.create({
onError: (error, jobName) => report(jobName, error),
});
baker.add({ name: "a", cron: "@hourly", callback }); // uses the baker's handler
baker.add({
name: "b",
cron: "@hourly",
callback,
onError: (error) => report("b", error), // the baker's handler isn't called
});Errors inside hooks
Hooks can’t take a job down either.
- If
onTickoronCompletethrows, the error goes tologger.warn(). A failingonTickdoesn’t change the result of the run, so it still counts as a success. - If
onErrorthrows, that error also goes tologger.warn().
Hooks count towards the run
onTick and onError are awaited as part of the run. The recorded duration includes them, and with overlap protection the job isn’t free to start again until they finish. Keep them quick, or don’t return the promise if the work can happen in the background.
Retrying
Cronbake doesn’t retry a failed run. The next attempt is the next tick. If a job needs retries, wrap the work yourself.
async function withRetries(work: () => Promise<void>, attempts = 3) {
for (let attempt = 1; ; attempt++) {
try {
return await work();
} catch (error) {
if (attempt >= attempts) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** attempt));
}
}
}
baker.add({
name: "sync-invoices",
cron: "@every_30_minutes",
callback: () => withRetries(syncInvoices),
});The final failure still reaches onError and the metrics, and overlap protection covers the whole retry sequence.