Skip to main content
When a processor throws an exception that is considered unrecoverable, you should use the UnrecoverableError class. In this case, BullMQ will just move the job to the failed set without performing any retries, overriding any attempts settings used when adding the job to the queue.

Basic Usage

import { Worker, UnrecoverableError } from 'bullmq';

const worker = new Worker(
  'foo',
  async job => {
    doSomeProcessing();
    throw new UnrecoverableError('Unrecoverable');
  },
  { connection },
);

await queue.add(
  'test-retry',
  { foo: 'bar' },
  {
    attempts: 3,
    backoff: 1000,
  },
);
Even though the job is configured with 3 attempts, throwing UnrecoverableError will cause it to fail immediately without any retries.

When to Use UnrecoverableError

Invalid Input

The job data is malformed or invalid and will never succeed

Missing Resources

A required file or resource doesn’t exist and won’t appear later

Business Logic Failure

The operation violates business rules and shouldn’t be retried

Authorization Errors

Permanent authorization failures that won’t resolve with retries

Fail Job with Manual Rate Limit

When a job is rate limited using RateLimitError and tried again, the attempts check is ignored, as rate limiting is not considered a real error. However, if you want to manually check the attempts and avoid retrying the job, you can check job.attemptsStarted:
import { Worker, RateLimitError, UnrecoverableError } from 'bullmq';

const worker = new Worker(
  'myQueue',
  async job => {
    const [isRateLimited, duration] = await doExternalCall();
    if (isRateLimited) {
      await queue.rateLimit(duration);
      if (job.attemptsStarted >= job.opts.attempts) {
        throw new UnrecoverableError('Unrecoverable');
      }
      // Do not forget to throw this special exception,
      // since we must differentiate this case from a failure
      // in order to move the job to wait again.
      throw new RateLimitError();
    }
  },
  {
    connection,
    limiter: {
      max: 1,
      duration: 500,
    },
  },
);
job.attemptsMade is increased when any error different than RateLimitError, DelayedError or WaitingChildrenError is thrown. While job.attemptsStarted is increased every time that a job is moved to active.

Example: Validation Failure

import { Worker, UnrecoverableError } from 'bullmq';
import Joi from 'joi';

const schema = Joi.object({
  email: Joi.string().email().required(),
  name: Joi.string().min(2).required(),
});

const worker = new Worker(
  'user-registration',
  async job => {
    // Validate job data
    const { error } = schema.validate(job.data);
    
    if (error) {
      // Invalid data - no point in retrying
      throw new UnrecoverableError(
        `Invalid job data: ${error.message}`
      );
    }
    
    // Process valid job
    await registerUser(job.data);
  },
  { connection },
);

Comparison: Error Types

Error TypeBehaviorUse Case
ErrorJob retries with backoffTemporary failures
UnrecoverableErrorJob fails immediatelyPermanent failures
RateLimitErrorJob waits, then retriesRate limit hit
DelayedErrorJob delays, then retriesManual delay needed
WaitingChildrenErrorJob waits for childrenParent-child dependencies

Retrying Failed Jobs

Learn about retry strategies and backoff

UnrecoverableError API

API reference for UnrecoverableError

Rate Limiting

Implement rate limiting for your queues

Rate Limit API

API reference for queue.rateLimit

Build docs developers (and LLMs) love