immich-app/immich · error · BadRequestException

not found

Error message

${entity} not found

What it means

findOrFail() is a helper that wraps an async lookup and converts a null/undefined result into a NestJS BadRequestException with '<entity> not found'. It centralizes not-found handling for repository lookups so callers get a clean 400 instead of propagating undefined.

Solutions

  1. Verify the requested entity ID exists (and belongs to the authenticated user) before the call.
  2. Refresh client-side state/list to drop stale references to deleted entities.
  3. If the entity is legitimately optional, use the raw find call instead of findOrFail and handle null yourself.
  4. Check the message prefix ('<entity> not found') to identify which lookup failed and fix the upstream data.
Defensive patterns

Strategy: try-catch

Validate before calling

const exists = asset && (await repo.get(asset.id)) !== null;
if (!exists) return null; // avoid triggering not-found

Type guard

const isNotFound = (e: unknown): e is BadRequestException =>
  e instanceof BadRequestException && / not found$/.test(e.message);

Try / catch

try { const user = await findOrFail(() => repo.get(id), 'User'); } catch (e) { if (isNotFound(e)) return res.status(404).json({ error: 'User not found' }); throw e; }

Prevention

When it happens

Trigger: Any code path calling findOrFail with a finder function whose query matched no row, e.g. requesting a nonexistent asset/user/album ID via an API endpoint.

Common situations: Client requesting an asset that was deleted server-side or belongs to another user; stale IDs cached in a client; typos in IDs; race where the entity is removed between listing and fetching.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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

Appendix: source

Thrown at server/src/utils/misc.ts:114

const isMachineLearningEnabled = (machineLearning: SystemConfig['machineLearning']) => machineLearning.enabled;
export const isSmartSearchEnabled = (machineLearning: SystemConfig['machineLearning']) =>
  isMachineLearningEnabled(machineLearning) && machineLearning.clip.enabled;
export const isOcrEnabled = (machineLearning: SystemConfig['machineLearning']) =>
  isMachineLearningEnabled(machineLearning) && machineLearning.ocr.enabled;
export const isFacialRecognitionEnabled = (machineLearning: SystemConfig['machineLearning']) =>
  isMachineLearningEnabled(machineLearning) && machineLearning.facialRecognition.enabled;
export const isDuplicateDetectionEnabled = (machineLearning: SystemConfig['machineLearning']) =>
  isSmartSearchEnabled(machineLearning) && machineLearning.duplicateDetection.enabled;
export const isFaceImportEnabled = (metadata: SystemConfig['metadata']) => metadata.faces.import;

export const handlePromiseError = <T>(promise: Promise<T>, logger: LoggingRepository): void => {
  promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack));
};

export const findOrFail = async <T>(find: () => Promise<T>, entity: string): Promise<NonNullable<T>> => {
  const value = await find();
  if (!value) {
    throw new BadRequestException(`${entity} not found`);
  }

  return value;
};

export async function* batched<T>(items: AsyncIterable<T>, size = JOBS_ASSET_PAGINATION_SIZE): AsyncGenerator<T[]> {
  let batch: T[] = [];

  for await (const item of items) {
    batch.push(item);

    if (batch.length >= size) {
      yield batch;
      batch = [];
    }
  }

  if (batch.length > 0) {

View on GitHub (pinned to e55ac299a4)