immich-app/immich · error · ImmichStartupError

Failed to find job handler for Job.

Error message

Failed to find job handler for Job.${jobKey} ("${jobName}")

What it means

After handler discovery, the job repository validates that every @OnJob-decorated item's job name has a registered handler. If an item references a JobName for which no handler exists (handlers are keyed by the resolved job name), setup throws an ImmichStartupError telling the developer to add the @OnJob decorator — this guards against jobs declared but never implemented.

Solutions

  1. Add the missing handler method decorated with @OnJob({ name: JobName.<KEY>, queue: QueueName.XYZ }) as the error message suggests
  2. Check for a JobName rename/mismatch: ensure the decorator name matches an enum value and the handler still exists after refactors
  3. Resolve any earlier 'Failed to add job handler' duplicates so the handler registers correctly
  4. Rebuild after changes (clean dist) to make sure the running code includes the new handler

Example fix

// before — job referenced but never handled
@OnJob({ name: JobName.QueueSync, queue: QueueName.Backup })
// no method anywhere handles JobName.QueueSync
// after
export class BackupService {
  @OnJob({ name: JobName.QueueSync, queue: QueueName.Backup })
  async handleQueueSync(job: Job<QueueSyncParameters>) {
    // ...
  }
}
Defensive patterns

Strategy: validation

Validate before calling

const declared = new Set<string>(getAllJobNamesReferencedByDecorators());
const handled = new Set<string>(Object.keys(jobHandlers));
const missing = [...declared].filter((n) => !handled.has(n));
if (missing.length > 0) throw new Error(`jobs without handlers: ${missing.join(', ')}`);

Try / catch

try {
  jobRepo.setup({ services });
} catch (e) {
  if (e instanceof ImmichStartupError && (e.message as string).startsWith('Failed to find job handler')) {
    logger.fatal(`${e.message} — add the missing @OnJob handler before deploying.`);
  }
  throw e;
}

Prevention

When it happens

Trigger: A @OnJob decorator uses a JobName whose handler registration was skipped or failed, or a job key was renamed/mistyped so the handlers map (keyed by string job name) has no entry for it — e.g. decorator name and enum value diverge after a refactor.

Common situations: Adding a new JobName enum value and a decorator but forgetting the handler method; renaming a JobName in one place only; a duplicated-handler error earlier preventing registration while the item still exists; typos in the decorator's name string.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/5f99d458086bc6f1. Report an issue: GitHub.

Appendix: source

Thrown at server/src/repositories/job.repository.ts:85

          label,
          jobName,
          queueName,
          handler: handler.bind(instance),
        };

        this.logger.verbose(`Added job handler: ${jobName} => ${label}`);
      }
    }

    // no missing handlers
    for (const [jobKey, jobName] of Object.entries(JobName)) {
      const item = this.handlers[jobName];
      if (!item) {
        const errorMessage = `Failed to find job handler for Job.${jobKey} ("${jobName}")`;
        this.logger.error(
          `${errorMessage}. Make sure to add the @OnJob({ name: JobName.${jobKey}, queue: QueueName.XYZ }) decorator for the new job.`,
        );
        throw new ImmichStartupError(errorMessage);
      }
    }
  }

  startWorkers() {
    const { bull } = this.configRepository.getEnv();
    for (const queueName of Object.values(QueueName)) {
      this.logger.debug(`Starting worker for queue: ${queueName}`);
      this.workers[queueName] = new Worker(
        queueName,
        (job) => this.eventRepository.emit('JobRun', queueName, job as JobItem),
        { ...bull.config, concurrency: 1, name: ImmichWorker.Microservices },
      );
    }
  }

  watchWorkers() {
    this.workerWatcher ??= setInterval(() => void this.checkWorkers(), WORKER_WATCH_INTERVAL_MS);

View on GitHub (pinned to e55ac299a4)