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:

  1. The error is caught, so it can’t crash your process.
  2. The run is recorded as failed in the job’s metrics and history, and lastError is updated.
  3. Exactly one error handler is called.
  4. The job stays running and 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.

  1. The job’s own onError.
  2. The baker’s onError, which also receives the job name.
  3. 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 onTick or onComplete throws, the error goes to logger.warn(). A failing onTick doesn’t change the result of the run, so it still counts as a success.
  • If onError throws, that error also goes to logger.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.

esc

Try overlap, restore or timezone.