immich-app/immich · error · BadRequestException
Cannot remove stack's primary asset
Error message
Cannot remove stack's primary asset
What it means
Immich throws this BadRequestException when removing an asset from a stack if that asset is the stack's primary asset. The primary asset anchors the stack and must be removed via unstacking or by first promoting another asset.
Solutions
- Choose a different asset in the stack as the removal target, or update the stack's primaryAssetId to another member first, then remove this asset.
- To remove the whole grouping, use DELETE /stacks/:id (unstack all) instead of removing the primary asset individually.
- Skip primary assets in bulk-removal loops.
- Handle the 400 by fetching the stack and testing primaryAssetId before the call.
Example fix
// before
await api.removeAssetFromStack(auth, stackId, stack.primaryAssetId);
// after
await api.updateStack(auth, stackId, { primaryAssetId: stack.assets.find(a => a.id !== stack.primaryAssetId).id });
await api.removeAssetFromStack(auth, stackId, originalPrimaryAssetId); Defensive patterns
Strategy: validation
Validate before calling
const stack = await api.getStack(auth, stackId);
if (stack.primaryAssetId === assetId) throw new Error('Promote another asset to primary before removing this one'); Type guard
const isPrimaryAsset = (stack: { primaryAssetId: string }, assetId: string) => stack.primaryAssetId === assetId; Try / catch
try {
await api.removeAssetFromStack(auth, stackId, assetId);
} catch (e) {
if (e.status === 400 && /primary asset/.test(e.message)) {
await api.updateStack(auth, stackId, { primaryAssetId: otherAssetId });
await api.removeAssetFromStack(auth, stackId, assetId);
} else throw e;
} Prevention
- Exclude primaryAssetId from bulk removal lists
- Promote a replacement primary before removing the cover asset
- Use full unstack (delete stack) when dissolving a group
- Check primaryAssetId before any per-asset stack mutation
When it happens
Trigger: DELETE /stacks/:id/assets where assetId === stack.primaryAssetId.
Common situations: Bulk removal scripts iterating all stack assets including the cover; UI defaulting to the first (primary) asset; trying to dissolve a stack one asset at a time starting with the primary.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Asset not in stack
- Primary asset must be in the stack
- Cannot leave a cluster group without any other members
- Invalid shared link type
- Asset dimensions are not available for editing
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/36330013d3159d9e.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/stack.service.ts:74
async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
await this.requireAccess({ auth, permission: Permission.StackDelete, ids: dto.ids });
await this.stackRepository.deleteAll(dto.ids);
await this.eventRepository.emit('StackDeleteAll', { stackIds: dto.ids, userId: auth.user.id });
}
async removeAsset(auth: AuthDto, dto: UUIDAssetIDParamDto): Promise<void> {
const { id: stackId, assetId } = dto;
await this.requireAccess({ auth, permission: Permission.StackUpdate, ids: [stackId] });
const stack = await this.stackRepository.getForAssetRemoval(assetId);
if (!stack?.id || stack.id !== stackId) {
throw new BadRequestException('Asset not in stack');
}
if (stack.primaryAssetId === assetId) {
throw new BadRequestException("Cannot remove stack's primary asset");
}
await this.assetRepository.update({ id: assetId, stackId: null });
await this.eventRepository.emit('StackUpdate', { stackId, userId: auth.user.id });
}
private findOrFail(id: string) {
return findOrFail(() => this.stackRepository.getById(id), 'Asset stack');
}
}
View on GitHub (pinned to e55ac299a4)