immich-app/immich · warning · BadRequestException

Cannot remove stack's primary asset

Error message

Cannot remove stack's primary asset

What it means

StackService.removeAsset() refuses to remove the stack's primary asset: if stack.primaryAssetId === assetId it throws BadRequestException "Cannot remove stack's primary asset" (stack.service.ts:74, HTTP 400). The primary defines the stack's cover/representative and must be reassigned first.

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 199723261c)

Solutions

  1. First PATCH /stacks/:id to set primaryAssetId to a different member, then remove the old primary.
  2. In the UI, disable remove on the primary asset or prompt to reassign primary first.
  3. If you intend to dissolve the stack, DELETE /stacks/:id instead.

Example fix

// before
await removeAsset({ id: stackId, assetId: stack.primaryAssetId });
// after - reassign primary, then remove
await update(stackId, { primaryAssetId: otherMemberId });
await removeAsset({ id: stackId, assetId: oldPrimaryId });
Defensive patterns

Strategy: validation

Validate before calling

const stack = await stackApi.get(id);
if (stack.primaryAssetId === assetId) {
  const otherMember = stack.assets.find((a) => a.id !== assetId);
  if (!otherMember) throw new Error('Cannot remove the only asset; delete the stack instead.');
  await stackApi.update(id, { primaryAssetId: otherMember.id });
}
await stackApi.removeAsset({ id, assetId });

Type guard

const isPrimaryAsset = (assetId: string, stack: { primaryAssetId: string }): boolean =>
  stack.primaryAssetId === assetId;

Try / catch

try {
  await stackApi.removeAsset({ id: stackId, assetId });
} catch (e) {
  if (e instanceof BadRequestException && /primary asset/i.test(e.message)) {
    // reassign primary first, then retry
    const other = stack.assets.find((a) => a.id !== assetId);
    if (other) {
      await stackApi.update(stackId, { primaryAssetId: other.id });
      return stackApi.removeAsset({ id: stackId, assetId });
    }
  }
  throw e;
}

Prevention

When it happens

Trigger: DELETE /stacks/:id/assets/:assetId targeting the asset that is currently the stack's primaryAssetId. The asset-in-stack check (176) passes first, then this guard fires.

Common situations: Bulk-removing assets including the primary, or a UI that lets users remove any asset without first reassigning the primary.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/36330013d3159d9e. Report an issue: GitHub.