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

  1. 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.
  2. To remove the whole grouping, use DELETE /stacks/:id (unstack all) instead of removing the primary asset individually.
  3. Skip primary assets in bulk-removal loops.
  4. 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

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


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)