When the processor throws.
A failed attempt is not a lost job. bunqueue counts the attempt, waits a widening backoff, tries again, and only after the budget runs out does the job become permanently dead.
Handle errors
Section titled “Handle errors”Throwing inside the processor fails the current attempt; bunqueue retries with backoff (a growing wait between attempts) until attempts is exhausted:
const worker = new Worker('queue', async (job) => { await riskyOperation(); // Just let errors throw, retries are automatic}, { embedded: true });
worker.on('failed', (job, error) => { console.warn(`Attempt ${job.attemptsMade + 1} failed`, error);});const worker = new Worker('queue', async (job) => { await riskyOperation(); // Just let errors throw, retries are automatic});
worker.on('failed', (job, error) => { const finalAttempt = job.attempts + 1 >= job.maxAttempts; if (finalAttempt) alertOps(job, error);});def process(job): risky_operation() # Just let exceptions raise, retries are automatic
worker = Worker("queue", process)
def on_failed(job, error): final_attempt = job.attempts + 1 >= job.max_attempts if final_attempt: alert_ops(job, error)
worker.on("failed", on_failed)$worker = new Worker('queue', function (Bunqueue\Job $job) { riskyOperation(); // Just let exceptions throw, retries are automatic});
$worker->on('failed', function ($job, $error) { // This event reports every failed attempt; inspect the DLQ for terminal jobs. logAttemptFailure($job, $error);});worker := bunqueue.NewWorker("queue", func(job *bunqueue.Job) (any, error) { return nil, riskyOperation() // Return an error, retries are automatic}, bunqueue.WorkerOptions{})
worker.On("failed", func(args ...any) { job := args[0].(*bunqueue.Job) // This event reports every failed attempt; inspect the DLQ for terminal jobs. logAttemptFailure(job, args[1])})use bunqueue_client::{ProcessError, Worker, WorkerOptions};
let worker = Worker::new( "queue", |job| { risky_operation(&job) .map_err(|error| ProcessError::retryable(error.to_string())) // ProcessError::unrecoverable(...) skips retries -> DLQ }, WorkerOptions::default(),);worker = Bunqueue.Worker.new("queue", fn job -> # Return {:error, reason} (or raise), retries are automatic. # Raising Bunqueue.UnrecoverableError skips retries -> DLQ. risky_operation(job) end)The failed listener fires after each broker-confirmed failed attempt, not only
after retry exhaustion. The job snapshot was pulled before that failure, so the
attempt that just failed is attemptsMade + 1 (Bun), attempts + 1
(TypeScript/Python). PHP and Go do not expose maxAttempts on their Job view;
use the queue’s DLQ operations for authoritative terminal-failure alerts. Rust
and Elixir record per-job outcomes in normal processor control flow and expose
transport/retry failures through telemetry; see the
SDK guide.
A processing timeout can win while user code is still running. In that race,
the broker has already recorded the failed attempt. The Bun Worker treats the
late processor result or exception as an acknowledged no-op: it emits neither
a second completed/failed event nor a Worker error, and a newer retry lease
remains free to finish normally.
Where to go next
Section titled “Where to go next”| Worker | Create a worker and process your first job |
| Worker Concurrency and Batch Pulling | Run jobs in parallel and pull them in batches |
| The Job Object Inside a Worker Processor | Everything the processor receives and can do |
| Worker Events | completed, failed, stalled and the rest |
| Worker Lifecycle | Pause, resume and shut down without losing work |
| Heartbeats, Stall Detection and Lock Ownership | Heartbeats, stall recovery and lock ownership |
| SandboxedWorker | Experimental isolation for CPU-heavy handlers |
| WorkerOptions Reference | Every WorkerOptions field, with defaults |