d2lang/d2 · error

invalid underscore

Error message

invalid underscore

What it means

During edge id resolution in d2ir, when an underscore reference (`_`) is expanded the algorithm walks up to the parent map to resolve the edge endpoint. If the parent map is nil (no further scope to climb), the underscore reference is unresolvable and this error is thrown.

Source

Thrown at d2ir/d2ir.go:746

func (eid *EdgeID) resolve(m *Map) (_ *EdgeID, _ *Map, common []d2ast.String, _ error) {
	eid = eid.Copy()
	maxUnderscores := go2.Max(countUnderscores(eid.SrcPath), countUnderscores(eid.DstPath))
	for i := 0; i < maxUnderscores; i++ {
		if eid.SrcPath[0].ScalarString() == "_" && eid.SrcPath[0].IsUnquoted() {
			eid.SrcPath = eid.SrcPath[1:]
		} else {
			mf := ParentField(m)
			eid.SrcPath = append([]d2ast.String{mf.Name}, eid.SrcPath...)
		}
		if eid.DstPath[0].ScalarString() == "_" && eid.DstPath[0].IsUnquoted() {
			eid.DstPath = eid.DstPath[1:]
		} else {
			mf := ParentField(m)
			eid.DstPath = append([]d2ast.String{mf.Name}, eid.DstPath...)
		}
		m = ParentMap(m)
		if m == nil {
			return nil, nil, nil, errors.New("invalid underscore")
		}
	}

	for len(eid.SrcPath) > 1 && len(eid.DstPath) > 1 {
		if !strings.EqualFold(eid.SrcPath[0].ScalarString(), eid.DstPath[0].ScalarString()) || strings.Contains(eid.SrcPath[0].ScalarString(), "*") {
			return eid, m, common, nil
		}
		common = append(common, eid.SrcPath[0])
		eid.SrcPath = eid.SrcPath[1:]
		eid.DstPath = eid.DstPath[1:]
	}

	return eid, m, common, nil
}

type Edge struct {
	// *Map
	parent    Node

View on GitHub (pinned to 0d69dca6f5)

Solutions

  1. Fix the edge path so the `_` reference has a valid parent scope; use absolute names if unsure.
  2. Ensure the edge is declared inside the nested scope that the `_` refers to, not at a boundary where no parent exists.
  3. Replace `_`-prefixed endpoints with explicit full paths.

Example fix

// before (edge in a scope with no resolvable parent)
_.x -> y
// after
root.x -> y
Defensive patterns

Strategy: validation

Validate before calling

// Ensure underscore edges are declared in a nested scope that has a parent field chain
func edgeScopeResolvable(scope *d2ir.Map, edgeKey string) bool {
  return !strings.Contains(edgeKey, "_") || scope != nil && ParentField(scope) != nil || ParentMap(scope) != nil
}

Type guard

func hasParentScope(m *d2ir.Map) bool { return ParentMap(m) != nil }

Try / catch

eid, _, _, err := d2ir.EidID(...) // or via compile
if err != nil && err.Error() == "invalid underscore" {
  // replace _-prefixed endpoints with absolute names and recompile
}

Prevention

When it happens

Trigger: An edge key containing `_` whose endpoint cannot be resolved because ParentMap(m) is nil — e.g. an edge declared in a scope where the `_` prefix climbs above the root or in a context (like inside a connection's own field) with no parent field chain to resolve against.

Common situations: Malformed relative edges like `(a-b)._x` or underscore edges at unexpected nesting; refactoring that moved an edge with `_.` prefixes into a shallower scope; typos in connection keys using `_` incorrectly.

Related errors


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