immich-app/immich · error · BadRequestException
Asset not in stack
Error message
Asset not in stack
What it means
Immich throws this BadRequestException when attempting to remove an asset from a stack that does not actually contain it — the stack fetched for the asset (getForAssetRemoval) is missing or its id differs from the target stackId in the request.
Solutions
- Confirm the asset is stacked and note its actual stackId (GET /asset/:id or asset.stackedAssets).
- Call the endpoint with the stack id that actually contains the asset.
- Refresh stack data before retrying if another client may have changed stacking.
- Check argument order — dto takes { id (stack), assetId } — and ensure they aren't swapped.
Example fix
// before await api.removeAssetFromStack(auth, oldStackId, assetId); // after const asset = await api.getAssetInfo(auth, assetId); const actualStackId = asset.stack?.id ?? asset.stackId; await api.removeAssetFromStack(auth, actualStackId, assetId);
Defensive patterns
Strategy: validation
Validate before calling
const asset = await api.getAssetInfo(auth, assetId);
if (asset.stackId !== stackId) throw new Error(`Asset ${assetId} is not in stack ${stackId}`); Type guard
const assetIsInStack = (asset: { stackId?: string | null }, stackId: string) => asset.stackId === stackId; Try / catch
try {
await api.removeAssetFromStack(auth, stackId, assetId);
} catch (e) {
if (e.status === 400 && /Asset not in stack/.test(e.message)) {
// refetch asset to get its real stackId, then retry
} else throw e;
} Prevention
- Resolve the asset's actual stackId from the asset record before calling
- Avoid caching (stackId, assetId) pairs across sessions
- Refresh stack state after any stacking operation
- Check dto argument order { id, assetId }
When it happens
Trigger: DELETE /stacks/:id/assets with an assetId whose actual stack differs from the :id path parameter, the asset is not stacked at all, or ids are swapped/mistyped.
Common situations: Client using a stale stack id after the asset was moved to another stack; passing (stackId, assetId) in the wrong order; concurrent unstacking by another client or job.
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
- Cannot remove stack's primary asset
- Primary asset must be in the stack
- Invalid shared link type
- Asset dimensions are not available for editing
- Asset does not have valid dimensions
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/de50fa3fb471cf54.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/stack.service.ts:70
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);
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)