{"record":{"id":"36ca431cd9747e79","repo":"d2lang/d2","slug":"can-only-delete-one-edge-at-a-time","errorCode":null,"errorMessage":"can only delete one edge at a time","messagePattern":"can only delete one edge at a time","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"d2oracle/edit.go","lineNumber":947,"sourceCode":"\t}\n\tif !m.IsFileMap() && m.Range.OneLine() {\n\t\t// This doesn't require any shenanigans to prevent consuming sibling spacing because\n\t\t// d2format will use the mapkey's range to determine whether to insert extra newlines.\n\t\t// See TestCreate/make_scope_multiline_spacing_2\n\t\tm.Range.End.Line++\n\t}\n}\n\nfunc Delete(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph, err error) {\n\tdefer xdefer.Errorf(&err, \"failed to delete %#v\", key)\n\n\tmk, err := d2parser.ParseMapKey(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(mk.Edges) > 1 {\n\t\treturn nil, errors.New(\"can only delete one edge at a time\")\n\t}\n\n\tif len(mk.Edges) == 1 {\n\t\tedgeTrimCommon(mk)\n\t}\n\n\tboardG := g\n\tbaseAST := g.AST\n\n\tif len(boardPath) > 0 {\n\t\t// When compiling a nested board, we can read from boardG but only write to baseBoardG\n\t\tboardG = GetBoardGraph(g, boardPath)\n\t\tif boardG == nil {\n\t\t\treturn nil, fmt.Errorf(\"board %v not found\", boardPath)\n\t\t}\n\t\t// TODO beter name\n\t\tbaseAST = boardG.BaseAST\n\t\tif baseAST == nil {","sourceCodeStart":929,"sourceCodeEnd":965,"githubUrl":"https://github.com/d2lang/d2/blob/0d69dca6f532ceaeacd615d35d1eaa41a238ffdb/d2oracle/edit.go#L929-L965","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Delete one edge at a time using an index: `(a -> b -> c)[0]`, then `[1]` (recomputing indices after each delete).","Issue multiple Delete calls, one per edge segment.","Pre-check len(mk.Edges) <= 1 by parsing the key with d2parser.ParseMapKey before calling Delete.","If deleting a chain, delete its composing edges individually to keep indices valid."],"exampleFix":"// before\ng, err = d2oracle.Delete(g, nil, \"(a -> b -> c)\")\n// after\ng, err = d2oracle.Delete(g, nil, \"(a -> b -> c)[1]\")\ng, err = d2oracle.Delete(g, nil, \"(a -> b -> c)[0]\")","handlingStrategy":"validation","validationCode":"mk, err := d2parser.ParseMapKey(key)\nif err != nil { return err }\nif len(mk.Edges) > 1 {\n    return fmt.Errorf(\"split %q: Delete handles one edge per call, use [index]\", key)\n}","typeGuard":"func isSingleEdgeKey(key string) bool {\n    mk, err := d2parser.ParseMapKey(key)\n    return err == nil && len(mk.Edges) <= 1\n}","tryCatchPattern":"_, err := d2oracle.Delete(g, nil, key)\nif err != nil && err.Error() == \"can only delete one edge at a time\" {\n    return fmt.Errorf(\"delete edges of %q one indexed key at a time\", key)\n}","preventionTips":["Delete chains segment by segment with explicit [index] keys","Recompute indices after each delete since they shift","Validate keys with ParseMapKey before invoking Delete","Do not assume Delete removes an entire chain"],"tags":["edge","delete","api-misuse","d2"],"backgroundTag":"multiple-edges-not-allowed","analyzedSha":"0d69dca6f532ceaeacd615d35d1eaa41a238ffdb","analyzedAt":"2026-08-31T12:19:21.182Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}