d2lang/d2 · error

no layout resolver configured for layout engine

Error message

no layout resolver configured for layout engine %q

What it means

d2lib's getLayout resolves the layout engine name through an optional LayoutResolver function in CompileOptions. If a Layout name was provided but no LayoutResolver was configured, there is no way to map the name to an implementation, so compilation fails with this error.

Solutions

  1. Set CompileOptions.LayoutResolver to a resolver such as d2plugin.NativeResolver or your custom mapping function
  2. Omit Layout to use the default path instead of naming an engine you cannot resolve
  3. Use the higher-level d2lib helpers that wire the resolver automatically

Example fix

// before
opts := &d2lib.CompileOptions{Layout: &layoutName}
// after
opts := &d2lib.CompileOptions{
	Layout: &layoutName,
	LayoutResolver: d2plugin.NativeResolver,
}
Defensive patterns

Strategy: validation

Validate before calling

if opts.Layout != nil && opts.LayoutResolver == nil {
	return errors.New("Layout set but LayoutResolver is nil")
}

Type guard

func optionsReady(opts *d2lib.CompileOptions) bool {
	return opts.Layout == nil || opts.LayoutResolver != nil
}

Try / catch

if _, err := d2lib.Compile(ctx, input, opts); err != nil {
	if strings.Contains(err.Error(), "no layout resolver configured") {
		opts.LayoutResolver = d2plugin.NativeResolver
		_, err = d2lib.Compile(ctx, input, opts)
	}
	return err
}

Prevention

When it happens

Trigger: Calling d2lib.Compile with CompileOptions.Layout set to a non-nil engine name (e.g. "dagre" or "elk") while leaving CompileOptions.LayoutResolver nil.

Common situations: Custom compile setups in embedding code (plugins, LSP tools) that set Layout but forget the resolver; tests constructing CompileOptions manually; copying options structs partially between call sites.

Related errors


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

Appendix: source

Thrown at d2lib/d2.go:199

	}
	previous := geometry
	for i, l := range g.Steps {
		ld, stepGeometry, err := compileBoard(ctx, l, compileOpts, renderOpts, previous, i+1 < len(g.Steps))
		if err != nil {
			return nil, nil, err
		}
		d.Steps = append(d.Steps, ld)
		previous = stepGeometry
	}
	return d, geometry, nil
}

func getLayout(opts *CompileOptions) (d2graph.LayoutGraph, error) {
	if opts.Layout == nil {
		return nil, errors.New("no available layout")
	}
	if opts.LayoutResolver == nil {
		return nil, fmt.Errorf("no layout resolver configured for layout engine %q", *opts.Layout)
	}
	return opts.LayoutResolver(*opts.Layout)
}

func getEdgeRouter(opts *CompileOptions) (d2graph.RouteEdges, error) {
	if opts.Layout != nil && opts.RouterResolver != nil {
		router, err := opts.RouterResolver(*opts.Layout)
		if err != nil {
			return nil, err
		}
		if router != nil {
			return router, nil
		}
	}
	return d2layouts.DefaultRouter, nil
}

// applyConfigs applies the configs read from D2 and applies it to passed in opts

View on GitHub (pinned to 0d69dca6f5)