dgraph-io/dgraph · error

Cycle detected

Error message

Cycle detected: %s

What it means

fragmentNode.expand expands a GraphQL+- fragment recursively; fn.Entered marks that the fragment is currently being expanded. If expand is re-entered for the same fragment before it exits, the query contains a fragment cycle (a fragment referencing itself directly or transitively), which would loop forever, so expansion aborts with this error naming the fragment.

Solutions

  1. Break the cycle by removing one of the mutual fragment spreads and inlining fields instead
  2. Rename fragments to spot duplicates — also check that two different fragments don't share a name causing fmap collisions
  3. Flatten the recursive structure: expand the deepest fragment's fields manually into its parent
  4. Test the query in Ratel/Dgraph console which reports the cyclic fragment name (given in the error's %s)

Example fix

// before
fragment A on User { ...B }
fragment B on User { ...A }
// after
fragment A on User { name }
fragment B on User { name age }
Defensive patterns

Strategy: try-catch

Validate before calling

func checkNoFragmentCycle(frags map[string][]string) error {
  var visit func(name string, seen map[string]bool) error
  visit = func(name string, seen map[string]bool) error {
    if seen[name] { return fmt.Errorf("cycle at fragment %s", name) }
    seen[name] = true
    for _, dep := range frags[name] {
      if err := visit(dep, seen); err != nil { return err }
    }
    delete(seen, name)
    return nil
  }
  for name := range frags {
    if err := visit(name, map[string]bool{}); err != nil { return err }
  }
  return nil
}

Try / catch

err := dql.ParseWithNeedVars(query, vars)
var xerr *errors.Error
if errors.As(err, &xerr) && strings.Contains(xerr.Error(), "Cycle detected") {
  // fragment name is embedded after 'Cycle detected: '
  log.Fatalf("mutually recursive fragment in query: %v", xerr)
}

Prevention

When it happens

Trigger: Running dql parsing (via ParseWithNeedVars -> expandFragments -> expand) on a query where fragment A spreads fragment B and B spreads A (or A spreads itself), e.g. 'fragment A on X { ...B } fragment B on X { ...A }'.

Common situations: Copy-pasting/merging fragment definitions in large queries; refactoring queries by splitting fragments and accidentally creating mutual references; generated queries from ORMs/tools with recursive templates.

Related errors


AI-assisted analysis of dgraph-io/dgraph@759e242be6 (2026-09-01). Data as JSON: /api/errors/a4b89f95f824c2a9. Report an issue: GitHub.

Appendix: source

Thrown at dql/parser.go:256

// DebugPrint is useful for debugging.
func (gq *GraphQuery) DebugPrint(prefix string) {
	glog.Infof("%s[%x %q %q]\n", prefix, gq.UID, gq.Attr, gq.Alias)
	for _, c := range gq.Children {
		c.DebugPrint(prefix + "|->")
	}
}

func (gq *GraphQuery) isFragment() bool {
	return gq.fragment != ""
}

func (fn *fragmentNode) expand(fmap fragmentMap) error {
	if fn.Exited {
		// This fragment node has already been expanded.
		return nil
	}
	if fn.Entered {
		return errors.Errorf("Cycle detected: %s", fn.Name)
	}
	fn.Entered = true
	if err := fn.Gq.expandFragments(fmap); err != nil {
		return err
	}
	fn.Exited = true
	return nil
}

func (gq *GraphQuery) expandFragments(fmap fragmentMap) error {
	// We have to make a copy of children to preserve order and replace
	// fragment references with fragment content. The copy is newChildren.
	var newChildren []*GraphQuery
	// Expand non-fragments. Do not append to gq.Children.
	for _, child := range gq.Children {
		if child.isFragment() {
			fname := child.fragment // Name of fragment being referenced.
			fchild := fmap[fname]

View on GitHub (pinned to 759e242be6)