zed-industries/zed · warning · Error::Internal

could not find call to decline

Error message

could not find call to decline

What it means

Thrown by decline_call when expected_room_id is Some but no pending room_participant row matches the filter (called user, unanswered, null answering_connection, matching room). The Some(room_id) argument is an assertion that a specific ring exists; when the row is gone the server refuses instead of silently succeeding. With expected_room_id None the same lookup returns Ok(None) instead of erroring.

Source

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

        expected_room_id: Option<RoomId>,
        user_id: UserId,
    ) -> Result<Option<TransactionGuard<proto::Room>>> {
        self.optional_room_transaction(|tx| async move {
            let mut filter = Condition::all()
                .add(room_participant::Column::UserId.eq(user_id))
                .add(room_participant::Column::AnsweringConnectionId.is_null());
            if let Some(room_id) = expected_room_id {
                filter = filter.add(room_participant::Column::RoomId.eq(room_id));
            }
            let participant = room_participant::Entity::find()
                .filter(filter)
                .one(&*tx)
                .await?;

            let participant = if let Some(participant) = participant {
                participant
            } else if expected_room_id.is_some() {
                return Err(anyhow!("could not find call to decline"))?;
            } else {
                return Ok(None);
            };

            let room_id = participant.room_id;
            room_participant::Entity::delete(participant.into_active_model())
                .exec(&*tx)
                .await?;

            let room = self.get_room(room_id, &tx).await?;
            Ok(Some((room_id, room)))
        })
        .await
    }

    pub async fn cancel_call(
        &self,
        room_id: RoomId,

View on GitHub (pinned to bc538def45)

Solutions

  1. Pass expected_room_id: None when you want a graceful no-op if the ring already vanished; keep Some(room_id) only when your UI is certain the ring is live
  2. Drop the incoming-call notification from the UI as soon as a cancel/room-updated event arrives so decline cannot be sent for a dead ring
  3. Treat this error as 'call already gone' and simply close the notification, not as a failure

Example fix

// before
client.decline_call(Some(room_id)).await?;

// after (tolerate vanished rings)
match client.decline_call(Some(room_id)).await {
    Ok(_) => {}
    Err(err) if err.to_string().contains("could not find call to decline") => {
        // ring was cancelled or expired; nothing to do
    }
    Err(err) => return Err(err),
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Only decline rings the client still believes are live.
if let Some(ring) = self.incoming_calls.get(&room_id) {
    client.decline_call(Some(room_id)).await?;
    self.incoming_calls.remove(&room_id);
} else {
    log::debug!("no live ring for room {room_id}; skipping decline");
}

Try / catch

match client.decline_call(Some(room_id)).await {
    Ok(_) => {}
    Err(err) if err.to_string().contains("could not find call to decline") => {
        self.incoming_calls.remove(&room_id); // ring vanished; benign
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: Declining a ring that the caller already cancelled (CancelCall deleted the row); declining after the ring timed out and was cleaned up; passing the wrong room_id from a stale incoming-call notification; double-clicking decline so the second request finds nothing.

Common situations: Race between the caller cancelling and the callee declining; UI showing a stale ring notification after the room state changed; automated tests declining fabricated calls.

Related errors


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