d2lang/d2 · error

cannot rename edge to node

Error message

cannot rename edge to node

What it means

MoveIDDeltas in d2oracle validates a rename target of an existing edge. When the renamed ID parses to exactly one edge, it tries to move that edge; the check for a zero-edge target is logically dead inside the len==1 branch, but the message communicates that renaming an edge to a plain node ID is not a supported move operation. The library refuses edge-to-node renames because the move logic only knows how to relocate an edge, not convert it.

Source

Thrown at d2oracle/edit.go:2882

						return nil, err
					}
					newAK := d2graph.Key(newMK.Key)
					conflictOldIDs[ch] = ch.ID
					conflictNewIDs[ch] = newAK[len(newAK)-1]
					newIDs = append(newIDs, d2format.Format(newMK.Key))
				} else {
					newIDs = append(newIDs, d2format.Format(hoistedMK.Key))
				}
			}
		}
	}

	if len(mk.Edges) > 1 {
		return nil, nil
	}
	if len(mk.Edges) == 1 {
		if len(mk.Edges) == 0 {
			return nil, errors.New("cannot rename edge to node")
		}
		if len(mk.Edges) > 1 {
			return nil, errors.New("cannot rename edge to edge chain")
		}

		e, ok := obj.HasEdge(mk)
		if !ok {
			return nil, nil
		}
		beforeID := e.AbsID()
		tmp := *e
		e2 := &tmp
		e2.SrcArrow = mk2.Edges[0].SrcArrow == "<"
		e2.DstArrow = mk2.Edges[0].DstArrow == ">"
		deltas[beforeID] = e2.AbsID()
		return deltas, nil
	}

View on GitHub (pinned to 0d69dca6f5)

Solutions

  1. Use the correct API for node renames: call MoveIDDeltas only when newName also parses to an edge with exactly one edge
  2. Parse newName with d2parser.ParseMapKey before calling and check len(mk.Edges)==1 to confirm it is still an edge
  3. Convert the edge into a node manually instead: delete the edge and create the node via separate oracle edit operations

Example fix

// before
renamed, err := oracle.MoveIDDeltas(ctx, g, "x -> y", "myNode", nil) // old id is edge, new is node
// after
mk2, _ := d2parser.ParseMapKey(newName)
if len(mk.Edges) == 1 {
    renamed, err = oracle.MoveIDDeltas(ctx, g, "x -> y", newName, nil)
} else {
    // handle node rename via a different path
}
Defensive patterns

Strategy: validation

Validate before calling

mk, err := d2parser.ParseMapKey(newName)
if err != nil { return err }
if len(mk.Edges) != 1 { return errors.New("rename of an edge requires a single-edge new name") }

Type guard

func isSingleEdge(newName string) bool {
    mk, err := d2parser.ParseMapKey(newName)
    return err == nil && mk != nil && len(mk.Edges) == 1
}

Try / catch

renamed, err := oracle.MoveIDDeltas(ctx, g, oldID, newName, nil)
if err != nil && strings.Contains(err.Error(), "cannot rename edge") {
    // fall back to delete-edge + add-node flow
}

Prevention

When it happens

Trigger: Calling MoveIDDeltas (directly or via updateNear) where the old ID is an edge (e.g. 'a -> b') and the parsed mk has exactly one edge, hitting the (unreachable-by-construction, defensive) len==0 guard inside the single-edge branch.

Common situations: Renaming an edge ID in a D2 script via the oracle API and accidentally supplying a node name instead of an edge expression; renaming between boards where the new name loses its edge arrow; tooling that reuses MoveIDDeltas for generic ID renames without checking edge-ness of the new name.

Related errors


AI-assisted analysis of d2lang/d2@0d69dca6f5 (2026-08-31). Data as JSON: /api/errors/762dfb0570a54092. Report an issue: GitHub.