zed-industries/zed · error · RpcError

CircularNesting

CircularNesting

Error message

CircularNesting

What it means

ErrorCode::CircularNesting from move_channel (crates/collab/src/db/queries/channels.rs:930): the new parent's ancestor chain (ancestors_including_self) contains the channel being moved, i.e. the new parent is the channel itself or one of its descendants. Nesting a channel inside its own subtree would create a cycle in the materialized parent_path.

Source

Thrown at crates/collab/src/db/queries/channels.rs:930

        channel_id: ChannelId,
        new_parent_id: ChannelId,
        admin_id: UserId,
    ) -> Result<(ChannelId, Vec<Channel>)> {
        self.transaction(|tx| async move {
            let channel = self.get_channel_internal(channel_id, &tx).await?;
            self.check_user_is_channel_admin(&channel, admin_id, &tx)
                .await?;
            let new_parent = self.get_channel_internal(new_parent_id, &tx).await?;

            if new_parent.root_id() != channel.root_id() {
                Err(anyhow!(ErrorCode::WrongMoveTarget))?;
            }

            if new_parent
                .ancestors_including_self()
                .any(|id| id == channel.id)
            {
                Err(anyhow!(ErrorCode::CircularNesting))?;
            }

            if channel.visibility == ChannelVisibility::Public
                && new_parent.visibility != ChannelVisibility::Public
            {
                Err(anyhow!(ErrorCode::BadPublicNesting))?;
            }

            let root_id = channel.root_id();
            let new_parent_path = new_parent.path();
            let old_path = format!("{}{}/", channel.parent_path, channel.id);
            let new_path = format!("{}{}/", new_parent_path, channel.id);
            let new_order = max_order(&new_parent_path, &tx).await? + 1;

            let mut model = channel.into_active_model();
            model.parent_path = ActiveValue::Set(new_parent.path());
            model.channel_order = ActiveValue::Set(new_order);
            let channel = model.update(&*tx).await?;

View on GitHub (pinned to bc538def45)

Solutions

  1. Client-side, reject any drop target whose ancestor chain includes the dragged channel before sending the RPC
  2. Re-fetch the channel tree after any successful move so subsequent moves validate against fresh parent paths
  3. To swap hierarchy levels, first move the child out of the subtree to a neutral sibling, then perform the second move

Example fix

// before
client.move_channel(channel_id, channel_id /*self*/, admin_id).await?;

// after
let ok = !new_parent
    .ancestors_including_self()
    .any(|id| id == channel.id);
assert!(ok, "circular move");
client.move_channel(channel_id, new_parent_id, admin_id).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Reject drops onto the channel itself or any descendant
let circular = new_parent
    .ancestors_including_self()
    .any(|id| id == channel.id);
if circular {
    return Err(anyhow!("cannot move a channel into its own subtree"));
}

Try / catch

match db.move_channel(channel_id, new_parent_id, admin_id).await {
    Ok(v) => Ok(v),
    Err(err) if err.to_string().contains("CircularNesting") => {
        Err(anyhow!("move would create a cycle; pick a target outside the subtree"))
    }
    Err(err) => Err(err),
}

Prevention

When it happens

Trigger: move_channel where new_parent_id == channel_id, or new_parent is a direct child/descendant of the moved channel — classic when swapping a parent and child ('move A into B' where B is already inside A).

Common situations: Drag-and-drop races with stale tree state: the UI moves B under A, then a queued move puts A under B; two admins restructuring concurrently; client caching that still shows the pre-move hierarchy.

Related errors


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