immich-app/immich · error · BadRequestException
Primary asset must be in the stack
Error message
Primary asset must be in the stack
What it means
Immich throws this BadRequestException in StackService.update when the requested primaryAssetId is not one of the assets belonging to the stack. The primary asset must be a member of the stack being updated.
Solutions
- Fetch the stack (GET /stacks/:id) and pick primaryAssetId from its assets array.
- Verify the asset's stackId matches the stack before updating.
- If the asset should join the stack first, add it to the stack, then set it as primary.
- Refresh client-side stack data before retrying to clear stale ids.
Example fix
// before
await api.updateStack(auth, stackId, { primaryAssetId: someAssetId });
// after
const stack = await api.getStack(auth, stackId);
if (stack.assets.some(a => a.id === someAssetId)) {
await api.updateStack(auth, stackId, { primaryAssetId: someAssetId });
} Defensive patterns
Strategy: validation
Validate before calling
const stack = await api.getStack(auth, stackId);
if (!stack.assets.some(a => a.id === primaryAssetId)) throw new Error('primaryAssetId must be a member of the stack'); Type guard
const isInStack = (stack: { assets: { id: string }[] }, assetId: string) =>
stack.assets.some(a => a.id === assetId); Try / catch
try {
await api.updateStack(auth, stackId, { primaryAssetId });
} catch (e) {
if (e.status === 400 && /Primary asset must be in the stack/.test(e.message)) {
// refetch stack, pick a valid asset id, retry
} else throw e;
} Prevention
- Always pick primaryAssetId from the stack's assets array
- Refetch stack data before updates to avoid stale ids
- Validate stack membership in UI before submitting
- Watch for concurrent unstack operations
When it happens
Trigger: PUT /stacks/:id with a primaryAssetId that references an asset not in the stack (wrong stack id, asset unstacked earlier, or an id from a different stack).
Common situations: Clients caching asset ids then the stack was modified concurrently; UI sending the currently-viewed asset's id instead of one inside the stack; bulk scripts pairing stack ids and asset ids from different sources.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Asset not in stack
- Cannot remove stack's primary asset
- Invalid shared link type
- Asset dimensions are not available for editing
- assetIds, albumId, or userId is required
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/637b04b69ec7a0f0.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/stack.service.ts:41
const stack = await this.stackRepository.create({ ownerId: auth.user.id }, dto.assetIds);
await this.eventRepository.emit('StackCreate', { stackId: stack.id, userId: auth.user.id });
return mapStack(stack, { auth });
}
async get(auth: AuthDto, id: string): Promise<StackResponseDto> {
await this.requireAccess({ auth, permission: Permission.StackRead, ids: [id] });
const stack = await this.findOrFail(id);
return mapStack(stack, { auth });
}
async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
await this.requireAccess({ auth, permission: Permission.StackUpdate, ids: [id] });
const stack = await this.findOrFail(id);
if (dto.primaryAssetId && stack.assets.every(({ id }) => id !== dto.primaryAssetId)) {
throw new BadRequestException('Primary asset must be in the stack');
}
const updatedStack = await this.stackRepository.update(id, { id, primaryAssetId: dto.primaryAssetId });
await this.eventRepository.emit('StackUpdate', { stackId: id, userId: auth.user.id });
return mapStack(updatedStack, { auth });
}
async delete(auth: AuthDto, id: string): Promise<void> {
await this.requireAccess({ auth, permission: Permission.StackDelete, ids: [id] });
await this.stackRepository.delete(id);
await this.eventRepository.emit('StackDelete', { stackId: id, userId: auth.user.id });
}
async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
await this.requireAccess({ auth, permission: Permission.StackDelete, ids: dto.ids });
await this.stackRepository.deleteAll(dto.ids);View on GitHub (pinned to e55ac299a4)