d2lang/d2 · error
cannot generate unique key for edge chain
Error message
cannot generate unique key for edge chain
What it means
generateUniqueKey derives a fresh unique key for new elements, but it only supports single edges. If the requested prefix contains a chain of edges (len(mk.Edges) > 1, e.g. 'a -> b -> c'), no unique key can be generated and it returns this error. Called by Create, Rename, move, MoveIDDeltas and DeleteIDDeltas.
Source
Thrown at d2oracle/edit.go:2659
deltas[edge.AbsID()] = newEdge.AbsID()
return deltas, nil
}
// generateUniqueKey generates a unique key by appending a number after `prefix` such that it doesn't conflict with any IDs in `g`
// If `ignored` is not nil, a conflict with the ignored object is allowed. An example use case is to generate a unique ID for a child being
// hoisted out of its container, and you know the container is going to be deleted.
//
// If `included` is not nil, the generated key must also not conflict with a key in `included`, on top of not conflicting with any IDs in `g`.
// This is for when an operation needs to generate multiple unique keys in one go, like deleting a container and giving new IDs to all children
func generateUniqueKey(g *d2graph.Graph, prefix string, ignored *d2graph.Object, included []string) (key string, edge bool, _ error) {
mk, err := d2parser.ParseMapKey(prefix)
if err != nil {
return "", false, err
}
if len(mk.Edges) > 1 {
return "", false, errors.New("cannot generate unique key for edge chain")
}
if len(mk.Edges) == 1 {
if mk.EdgeIndex == nil || mk.EdgeIndex.Int == nil {
mk.EdgeIndex = &d2ast.EdgeIndex{
Int: go2.Pointer(0),
}
}
edgeTrimCommon(mk)
obj := g.Root
if mk.Key != nil {
var ok bool
obj, ok = g.Root.HasChild(d2graph.Key(mk.Key))
if !ok {
return d2format.Format(mk), true, nil
}
}View on GitHub (pinned to 0d69dca6f5)
Solutions
- Split the chain into individual edges: create 'a -> b' and 'b -> c' as separate Create calls.
- Generate intermediate nodes so each key contains at most one edge.
- If you only need the chain as notation, let d2 parser expansion handle it at the language level rather than via d2oracle Create with a chained key.
- Validate len(mk.Edges) <= 1 after parsing before invoking any d2oracle edit API with an edge prefix.
Example fix
// before delta, err := d2oracle.Create(graph.Id, "a -> b -> c") // edge chain: error // after d1, err := d2oracle.Create(graph.Id, "a -> b") // apply d1 d2, err2 := d2oracle.Create(graph.Id, "b -> c")
Defensive patterns
Strategy: validation
Validate before calling
mk, err := d2parser.ParseMapKey(prefix)
if err != nil { return err }
if len(mk.Edges) > 1 {
return fmt.Errorf("split edge chain %q into single-edge keys before calling d2oracle", prefix)
} Type guard
func isEdgeChain(key string) bool {
mk, err := d2parser.ParseMapKey(key)
return err == nil && len(mk.Edges) > 1
} Try / catch
delta, err := d2oracle.Create(graph.Id, prefix)
if err != nil {
if err.Error() == "cannot generate unique key for edge chain" {
return splitAndCreateEdges(graph.Id, prefix) // create a -> b, then b -> c
}
return err
} Prevention
- Reject multi-edge prefixes at input time (forms, CLI)
- Split 'a -> b -> c' notation into separate single-edge creates
- Sanitize user-supplied keys with d2parser before any d2oracle edit
- Document single-edge-only constraint in tooling that builds keys
When it happens
Trigger: Calling d2oracle.Create (or triggering rename/move/delete deltas) with a prefix key containing two or more connected edge segments, such as 'a -> b -> c'.
Common situations: Users typing chained connection syntax into a create box; programmatic generation of multi-hop edge chains in one key; templates that emit 'a -> b -> c' shorthand expecting it to expand.
Related errors
- moving across scopes isn't supported for edges
- edgeKey must be an edge
- edgeKey must refer to an existing edge
- not found
- dimensions for edge label %#v not found
AI-assisted analysis of d2lang/d2@0d69dca6f5 (2026-08-31).
Data as JSON: /api/errors/3bc3eb2dbeb11728.
Report an issue: GitHub.