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

  1. Include at least one entry in albumUsers with role AlbumUserRole.Owner (normally the creator, using authUserId).
  2. If callers pass only collaborators, prepend { userId: authUserId, role: AlbumUserRole.Owner } in the calling service before create().
  3. Audit DTO-to-repository mapping so the owner role isn't dropped or defaulted to Editor/Viewer.
  4. 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

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


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)