immich-app/immich · error · BadRequestException
A tag with that name already exists
Error message
A tag with that name already exists
What it means
Thrown (as BadRequestException) by TagService.create when tagRepository.getByValue(userId, value) returns an existing tag. The composite value (parent path + name) must be unique per user, so any collision - including case-only differences routed through the same value - is rejected.
Source
Thrown at server/src/services/tag.service.ts:50
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] });
const { color } = dto;
const tag = await this.tagRepository.update(id, { color });
return mapTag(tag);
}
async upsert(auth: AuthDto, dto: TagUpsertDto) {
const tags = await upsertTags(this.tagRepository, { userId: auth.user.id, tags: dto.tags });View on GitHub (pinned to 199723261c)
Solutions
- Reuse the existing tag instead of creating a new one (GET /tags and search by value).
- Choose a distinct name or parent so the composite value differs.
- If the existing tag is soft-deleted, hard-delete or restore it first.
Example fix
// before
await tagsApi.create({ name: 'cats' }); // exists already
// after
const existing = await tagsApi.getAll().then(t => t.find(x => x.value === 'cats'));
if (existing) return existing;
await tagsApi.create({ name: 'cats' }); Defensive patterns
Strategy: validation
Validate before calling
const value = parent ? `${parent.value}/${dto.name}` : dto.name;
const dup = await tagRepository.getByValue(userId, value);
if (dup) { /* offer to reuse dup instead of POSTing */ } Try / catch
try { await tagsApi.create(dto); }
catch (e) {
if (e instanceof BadRequestException && /already exists/.test(e.message)) {
// switch to update-or-reuse flow for the existing tag
}
} Prevention
- Search existing tags by value before offering create.
- Compute the composite value client-side to preview collisions.
- Hard-delete or restore soft-deleted tags before reusing their values.
When it happens
Trigger: POST /tags with a name that, combined with the optional parent, reproduces an existing tag value for that user.
Common situations: User re-creates a tag they just deleted but soft-delete still holds the value; renaming attempt that collides; parent path produces an unintended duplicate.
Related errors
- Tag not found
- Email is not available
- Storage label already in use by another account
- Duplicate items are not allowed: "${key}"
- Email is not available
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/cd420f5490106102.
Report an issue: GitHub.