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

  1. Split the chain into individual edges: create 'a -> b' and 'b -> c' as separate Create calls.
  2. Generate intermediate nodes so each key contains at most one edge.
  3. 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.
  4. 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

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


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