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
- Use the correct API for node renames: call MoveIDDeltas only when newName also parses to an edge with exactly one edge
- Parse newName with d2parser.ParseMapKey before calling and check len(mk.Edges)==1 to confirm it is still an edge
- 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
- Parse the new name and count mk.Edges before any edge rename
- Never pass plain node names as newName when the old ID is an edge
- Centralize edge renames behind a helper that validates edge-ness
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
- cannot rename edge to edge chain
- edgeKey must be an edge
- edgeKey must refer to an existing edge
- edge not found
- newSrc not found
AI-assisted analysis of d2lang/d2@0d69dca6f5 (2026-08-31).
Data as JSON: /api/errors/762dfb0570a54092.
Report an issue: GitHub.