immich-app/immich · error
Album must have an owner
Error message
Album must have an owner
What it means
AlbumRepository.create() asserts that among the albumUsers passed in, at least one has role AlbumUserRole.Owner (the every(...) check requires NOT all users to be non-owner). This plain Error enforces the invariant that a newly created album always has exactly an owner entry; a plain insert without any owner row would leave the album ownerless. Note the check is written so it fires when no user in the list has the Owner role.
Solutions
- Include at least one entry in albumUsers with role AlbumUserRole.Owner (normally the creator, using authUserId).
- If callers pass only collaborators, prepend { userId: authUserId, role: AlbumUserRole.Owner } in the calling service before create().
- Audit DTO-to-repository mapping so the owner role isn't dropped or defaulted to Editor/Viewer.
- Fix the check itself if intent was 'at least one owner' versus current logic (every non-owner => throw) — consider a clearer assertion with the creator always inserted.
Example fix
// before
await albumRepo.create(album, assetIds, [
{ userId: friendId, role: AlbumUserRole.Editor },
], authUserId); // Error: Album must have an owner
// after
await albumRepo.create(album, assetIds, [
{ userId: authUserId, role: AlbumUserRole.Owner },
{ userId: friendId, role: AlbumUserRole.Editor },
], authUserId); Defensive patterns
Strategy: validation
Validate before calling
if (!albumUsers.some(u => u.role === AlbumUserRole.Owner)) {
albumUsers = [{ userId: authUserId, role: AlbumUserRole.Owner }, ...albumUsers];
} Type guard
function hasOwner(users: { role: AlbumUserRole }[]): boolean {
return users.some(u => u.role === AlbumUserRole.Owner);
} Try / catch
try {
await albumRepo.create(album, assetIds, albumUsers, authUserId);
} catch (e) {
if (e instanceof Error && e.message === 'Album must have an owner') {
await albumRepo.create(album, assetIds,
[{ userId: authUserId, role: AlbumUserRole.Owner }, ...albumUsers], authUserId);
} else throw e;
} Prevention
- Always seed the creator as an owner entry when building the albumUsers list.
- Add a repository/service-level validation that an owner exists before calling create().
- Map DTOs carefully so Owner roles are not downgraded to Editor/Viewer in bulk imports.
- Prefer storing the owner separately from shared users to make the invariant explicit.
When it happens
Trigger: Calling albumRepository.create(album, assetIds, albumUsers, authUserId) where every entry in albumUsers has role Editor or Viewer (none with AlbumUserRole.Owner); constructing the album share list without including the creator as an owner.
Common situations: Service code building shared-user lists from a DTO where the creator was omitted from the users array; a refactor or API change that stopped seeding the owner role for the authenticated user; bulk-import scripts inserting collaborators (editors/viewers) only.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Admin account does not exist
- Cannot remove the last album owner
- Column 'embedding' does not exist in table
- Could not find asset
- Could not retrieve dimension size of column
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/ba2af9b02febd720.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/repositories/album.repository.ts:318
.onConflict((oc) => oc.doNothing())
.execute();
}
@GenerateSql({
params: [
{ albumName: DummyValue.STRING },
[],
[{ userId: DummyValue.UUID, role: AlbumUserRole.Owner }, DummyValue.UUID],
],
})
async create(
album: Insertable<AlbumTable>,
assetIds: string[],
albumUsers: AlbumUserCreateDto[],
authUserId: string,
) {
if (albumUsers.every((u) => u.role !== AlbumUserRole.Owner)) {
throw new Error('Album must have an owner');
}
const userIds = albumUsers.map((u) => u.userId);
const roles = albumUsers.map((u) => u.role);
const result = await this.db
.with('album', (db) => db.insertInto('album').values(album).returningAll())
.with('album_user', (db) =>
db
.insertInto('album_user')
.expression((eb) =>
eb
.selectFrom('album')
.select(({ ref }) => [
ref('album.id').as('albumId'),
sql`unnest(${userIds}::uuid[])`.as('userId'),
sql`unnest(${roles}::album_user_role_enum[])`.as('role'),
]),View on GitHub (pinned to e55ac299a4)