immich-app/immich · error · BadRequestException

Tag not found

Error message

Tag not found

What it means

Thrown (as BadRequestException) by TagService.create when dto.parentId is supplied, the caller passes TagRead access on it, but tagRepository.get(parentId) returns null. The parent must exist before a child tag can be created, because the child's value is composed as `${parent.value}/${dto.name}`.

Source

Thrown at server/src/services/tag.service.ts:42

export class TagService extends BaseService {
  async getAll(auth: AuthDto) {
    const tags = await this.tagRepository.getAll(auth.user.id);
    return tags.map((tag) => mapTag(tag));
  }

  async get(auth: AuthDto, id: string): Promise<TagResponseDto> {
    await this.requireAccess({ auth, permission: Permission.TagRead, ids: [id] });
    const tag = await this.findOrFail(id);
    return mapTag(tag);
  }

  async create(auth: AuthDto, dto: TagCreateDto) {
    let parent;
    if (dto.parentId) {
      await this.requireAccess({ auth, permission: Permission.TagRead, ids: [dto.parentId] });
      parent = await this.tagRepository.get(dto.parentId);
      if (!parent) {
        throw new BadRequestException('Tag not found');
      }
    }

    const userId = auth.user.id;
    const value = parent ? `${parent.value}/${dto.name}` : dto.name;
    const duplicate = await this.tagRepository.getByValue(userId, value);
    if (duplicate) {
      throw new BadRequestException(`A tag with that name already exists`);
    }

    const { color } = dto;
    const tag = await this.tagRepository.create({ userId, value, color, parentId: parent?.id });

    return mapTag(tag);
  }

  async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise<TagResponseDto> {
    await this.requireAccess({ auth, permission: Permission.TagUpdate, ids: [id] });

View on GitHub (pinned to 199723261c)

Solutions

  1. Re-fetch the parent tag list and use a current parentId.
  2. Drop parentId to create a top-level tag instead.
  3. Verify the id format and trim whitespace before submit.

Example fix

// before
await tagsApi.create({ name: 'summer', parentId: staleId });
// after
const parent = await tagsApi.getAll().then(t => t.find(p => p.value === 'trips'))!;
await tagsApi.create({ name: 'summer', parentId: parent.id });
Defensive patterns

Strategy: validation

Validate before calling

const parent = dto.parentId ? await tagRepository.get(dto.parentId) : null;
if (dto.parentId && !parent) { /* show 'pick a valid parent' error, do not POST */ }

Type guard

function isExistingTag(t: { id: string } | null): t is { id: string } {
  return t !== null && typeof t.id === 'string';
}

Try / catch

try { await tagsApi.create(dto); }
catch (e) {
  if (e instanceof BadRequestException && e.message === 'Tag not found') {
    // refresh parent list, repick parentId, retry
  }
}

Prevention

When it happens

Trigger: POST /tags with parentId referencing a tag that was deleted, belongs to another user, or whose id is malformed.

Common situations: Parent tag was deleted between UI load and submit; client stored a stale parentId; the id was copy-pasted with trailing whitespace.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/72775df132722dd9. Report an issue: GitHub.