d2lang/d2 · error

can only delete one edge at a time

Error message

can only delete one edge at a time

What it means

d2oracle.Delete, like Set, handles only one edge per key. A key parsing to multiple edges (a chain such as `(a -> b -> c)`) is ambiguous for deletion, so Delete refuses it up front.

Source

Thrown at d2oracle/edit.go:947

	}
	if !m.IsFileMap() && m.Range.OneLine() {
		// This doesn't require any shenanigans to prevent consuming sibling spacing because
		// d2format will use the mapkey's range to determine whether to insert extra newlines.
		// See TestCreate/make_scope_multiline_spacing_2
		m.Range.End.Line++
	}
}

func Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph, err error) {
	defer xdefer.Errorf(&err, "failed to delete %#v", key)

	mk, err := d2parser.ParseMapKey(key)
	if err != nil {
		return nil, err
	}

	if len(mk.Edges) > 1 {
		return nil, errors.New("can only delete one edge at a time")
	}

	if len(mk.Edges) == 1 {
		edgeTrimCommon(mk)
	}

	boardG := g
	baseAST := g.AST

	if len(boardPath) > 0 {
		// When compiling a nested board, we can read from boardG but only write to baseBoardG
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, fmt.Errorf("board %v not found", boardPath)
		}
		// TODO beter name
		baseAST = boardG.BaseAST
		if baseAST == nil {

View on GitHub (pinned to 0d69dca6f5)

Solutions

  1. Delete one edge at a time using an index: `(a -> b -> c)[0]`, then `[1]` (recomputing indices after each delete).
  2. Issue multiple Delete calls, one per edge segment.
  3. Pre-check len(mk.Edges) <= 1 by parsing the key with d2parser.ParseMapKey before calling Delete.
  4. If deleting a chain, delete its composing edges individually to keep indices valid.

Example fix

// before
g, err = d2oracle.Delete(g, nil, "(a -> b -> c)")
// after
g, err = d2oracle.Delete(g, nil, "(a -> b -> c)[1]")
g, err = d2oracle.Delete(g, nil, "(a -> b -> c)[0]")
Defensive patterns

Strategy: validation

Validate before calling

mk, err := d2parser.ParseMapKey(key)
if err != nil { return err }
if len(mk.Edges) > 1 {
    return fmt.Errorf("split %q: Delete handles one edge per call, use [index]", key)
}

Type guard

func isSingleEdgeKey(key string) bool {
    mk, err := d2parser.ParseMapKey(key)
    return err == nil && len(mk.Edges) <= 1
}

Try / catch

_, err := d2oracle.Delete(g, nil, key)
if err != nil && err.Error() == "can only delete one edge at a time" {
    return fmt.Errorf("delete edges of %q one indexed key at a time", key)
}

Prevention

When it happens

Trigger: Calling d2oracle.Delete with a key containing two or more edges, e.g. `x.(a -> b -> c)` or `(a <-> b <-> c)`, without an index selecting a single edge.

Common situations: Deleting a whole chain expecting all segments to be removed; forwarding user-typed chain keys from an editor; programmatic key generation joining multiple connections into one key.

Related errors


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