{"record":{"id":"749c64292ca52961","repo":"immich-app/immich","slug":"cannot-remove-the-last-album-owner","errorCode":null,"errorMessage":"Cannot remove the last album owner","messagePattern":"Cannot remove the last album owner","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/album.service.ts","lineNumber":331,"sourceCode":"  }\n\n  async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise<void> {\n    if (userId === 'me') {\n      userId = auth.user.id;\n    }\n\n    const album = await this.findOrFail(id, auth.user.id, { withAssets: false });\n\n    const exists = album.albumUsers.find(({ user: { id } }) => id === userId);\n    if (!exists) {\n      throw new BadRequestException('Album not shared with user');\n    }\n\n    if (\n      exists.role === AlbumUserRole.Owner &&\n      album.albumUsers.filter(({ role }) => role === AlbumUserRole.Owner).length === 1\n    ) {\n      throw new BadRequestException('Cannot remove the last album owner');\n    }\n\n    // non-admin can remove themselves\n    if (auth.user.id !== userId) {\n      await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] });\n    }\n\n    await this.albumUserRepository.delete({ albumId: id, userId });\n  }\n\n  async updateUser(auth: AuthDto, id: string, userId: string, dto: UpdateAlbumUserDto): Promise<void> {\n    await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] });\n\n    const album = await this.findOrFail(id, userId, { withAssets: false });\n    const owner = album.albumUsers[0];\n\n    if (owner.user.id === userId) {\n      throw new BadRequestException('User is owner');","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/server/src/services/album.service.ts#L313-L349","documentation":"Raised by AlbumService.removeUser when the member being removed is an Owner and is the only owner in the album (albumUsers filtered to role Owner has length 1). This prevents deleting the last owner and leaving the album ownerless; ownership must first be transferred to another member via updateUser before removal.","triggerScenarios":"DELETE /albums/:id/user/:userId where the target is the sole Owner of the album (typically the album creator).","commonSituations":"Owners trying to remove themselves while still the only owner, or admins attempting to clean up members starting with the owner entry.","solutions":["The owner must transfer or delete the album instead of removing themselves","Promote/keep another owner first — though note addUsers cannot create owners, so owner removal is generally impossible by design","Remove non-owner members instead; the owner is removed only with album deletion"],"exampleFix":"// before\nawait api.removeAlbumUser(albumId, ownerId); // 400 if last owner\n// after\nif (album.ownerId !== userId) {\n  await api.removeAlbumUser(albumId, userId);\n} else {\n  await api.deleteAlbum(albumId); // owner leaves by deleting\n}","handlingStrategy":"validation","validationCode":"const album = await api.getAlbumInfo(albumId);\nconst target = album.albumUsers.find(m => m.userId === userId);\nif (target?.role === 'Owner') throw new Error('Cannot remove the last/only album owner');","typeGuard":"function isRemovableMember(userId: string, album: { albumUsers: { user: { id: string }; role: string }[] }): boolean {\n  const m = album.albumUsers.find(m => m.user.id === userId);\n  return !!m && m.role !== 'Owner';\n}","tryCatchPattern":"try {\n  await api.removeAlbumUser(albumId, userId);\n} catch (e) {\n  if ((e as Error).message === 'Cannot remove the last album owner') {\n    throw new Error('Delete the album or transfer ownership instead');\n  }\n  throw e;\n}","preventionTips":["Never offer a remove action for the owner row in UIs","Remove only Editor/Viewer members","Use album deletion for the owner to leave","Check the member's role before calling removeUser"],"tags":["album","permissions","invariant"],"backgroundTag":"invalid-state-transition","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}