zed-industries/zed · error

room does not exist or was already joined

Error message

room does not exist or was already joined

What it means

Thrown by join_room when the UPDATE that fills in participant_index and answering_connection_id on the pending room_participant row affects zero rows. That row is created by call(); if it no longer matches, the invitation is no longer open: the caller cancelled, the ring timed out, or the row was already answered (possibly by another connection of the same user).

Source

Thrown at crates/collab/src/db/queries/rooms.rs:342

                .filter(
                    Condition::all()
                        .add(room_participant::Column::RoomId.eq(room_id))
                        .add(room_participant::Column::UserId.eq(user_id))
                        .add(room_participant::Column::AnsweringConnectionId.is_null()),
                )
                .set(room_participant::ActiveModel {
                    participant_index: ActiveValue::Set(Some(participant_index)),
                    answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
                    answering_connection_server_id: ActiveValue::set(Some(ServerId(
                        connection.owner_id as i32,
                    ))),
                    answering_connection_lost: ActiveValue::set(false),
                    ..Default::default()
                })
                .exec(&*tx)
                .await?;
            if result.rows_affected == 0 {
                Err(anyhow!("room does not exist or was already joined"))?;
            }

            let room = self.get_room(room_id, &tx).await?;
            Ok(JoinRoom {
                room,
                channel: None,
            })
        })
        .await
    }

    pub async fn stale_room_connection(&self, user_id: UserId) -> Result<Option<ConnectionId>> {
        self.transaction(|tx| async move {
            let participant = room_participant::Entity::find()
                .filter(room_participant::Column::UserId.eq(user_id))
                .one(&*tx)
                .await?;
            Ok(participant.and_then(|p| p.answering_connection()))

View on GitHub (pinned to bc538def45)

Solutions

  1. Handle this error by telling the user the call ended and dropping the incoming-call state; offer to call back
  2. Ensure only one connection of the called user answers a given ring (pick one device, ignore the others)
  3. Refresh room state (get_room / room_updated events) after this failure so the UI resynchronizes

Example fix

// before
let join = client.join_room(room_id, connection_id).await?;

// after (recover from a dead ring)
let join = match client.join_room(room_id, connection_id).await {
    Ok(join) => join,
    Err(err) if err.to_string().contains("room does not exist or was already joined") => {
        self.incoming_calls.remove(&room_id);
        self.ui.show_toast("Call was cancelled or already answered");
        return Ok(());
    }
    Err(err) => return Err(err),
};
Defensive patterns

Strategy: fallback

Validate before calling

// Only answer rings that are still live and not answered elsewhere.
if let Some(ring) = self.incoming_calls.get(&room_id) {
    if ring.connection_id == my_connection_id && !ring.answered {
        client.join_room(room_id, my_connection_id).await?;
        return Ok(());
    }
}
log::debug!("not answering stale ring for room {room_id}");

Try / catch

match client.join_room(room_id, my_connection_id).await {
    Ok(join) => Ok(Some(join)),
    Err(err) if err.to_string().contains("room does not exist or was already joined") => {
        self.incoming_calls.remove(&room_id);
        self.ui.show_toast("Call was cancelled or already answered");
        Ok(None) // graceful fallback
    }
    Err(err) => Err(err),
}

Prevention

When it happens

Trigger: Answering a ring with JoinRoom after the caller hit cancel; answering after the ring expiry cleanup removed the row; two devices of the called user answer simultaneously and the second UPDATE matches nothing; answering a ring from a room you were since removed from.

Common situations: Slow user reaction to an incoming call notification; notification shown on two devices; network latency delivering the join after the caller hung up.

Related errors


AI-assisted analysis of zed-industries/zed@bc538def45 (2026-08-16). Data as JSON: /api/errors/f8d3b2a60aa04f62. Report an issue: GitHub.