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 by TagService.create when a tag with the same value already exists for the authenticated user. Tag values are unique per user, and child tags compose the full path (parentValue/name), so duplicates collide on the composed value. It is a 400 BadRequest signaling a uniqueness constraint violation.
Solutions
- Check for the existing tag first (GET /api/tags) and reuse it instead of creating
- Use upsert semantics: fetch by value; if found, return/update it, otherwise create
- Add client-side deduplication/normalization (trim, collapse slashes) before sending
- If a retry, treat this 400 as success and look up the previously created tag
Example fix
// before
const tag = await api.createTag({ name: 'travel' });
// after
const existing = tags.find(t => t.value === 'travel');
const tag = existing ?? await api.createTag({ name: 'travel' }); Defensive patterns
Strategy: validation
Validate before calling
const tags = await api.getAllTags();
if (tags.some(t => t.value === name)) {
return tags.find(t => t.value === name); // reuse instead of creating
} Try / catch
try { return await api.createTag({ name }); } catch (e) {
if (e.response?.status === 400 && /already exists/.test(e.response?.data?.message ?? '')) {
return (await api.getAllTags()).find(t => t.value === name);
}
throw e;
} Prevention
- Deduplicate names client-side before creation
- Normalize input (trim whitespace, collapse slashes)
- Make create idempotent: look up by value first
- Serialize create requests to avoid double submits
When it happens
Trigger: POST /api/tags with a name whose composed value (parent.value + '/' + name, or just name for root tags) matches an existing tag of the same user — including case-sensitivity quirks, hidden characters, or whitespace differences that still collide.
Common situations: Retrying a request after a timeout when the first call actually succeeded; importing tags from another instance; nested-tag name collisions where different parents' children share names but the composed path duplicates; users renaming parents so paths collide.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- Email is not available
- Storage label already in use by another account
- Cannot merge a person into themselves
- Duplicate items are not allowed
- Email is not available
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/cd420f5490106102.
Report an issue: GitHub.
Appendix: 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 { name, color } = dto;
const existing = await this.findOrFail(id);
let value;
if (name) {
const parts = existing.value.split('/');
parts[parts.length - 1] = name;View on GitHub (pinned to e55ac299a4)