amark/gun · error

Data at root of graph must be a node (an object).

Error message

Data at root of graph must be a node (an object).

What it means

Gun's put algorithm requires the root of the written data to be a graph node (a plain object). When the top-level value being put is a primitive (string, number, boolean, null) and there is no enclosing seen context (as.seen is undefined, i.e. this is the root of the put walk), the write is rejected: 'Data at root of graph must be a node (an object).'

Source

Thrown at src/put.js:38

	as.ran = as.ran || ran;
	//var path = []; as.via.back(at => { at.get && path.push(at.get.slice(0,9)) }); path = path.reverse().join('.');
	// TODO: Perf! We only need to stun chains that are being modified, not necessarily written to.
	(function walk(){
		var to = as.todo, at = to.pop(), d = at.it, cid = at.ref && at.ref._.id, v, k, cat, tmp, g;
		stun(as, at.ref);
		if(tmp = at.todo){
			k = tmp.pop(); d = d[k];
			if(tmp.length){ to.push(at) }
		}
		k && (to.path || (to.path = [])).push(k);
		if(!(v = valid(d)) && !(g = Gun.is(d))){
			if(!Object.plain(d)){ ran.err(as, "Invalid data: "+ check(d) +" at " + (as.via.back(function(at){at.get && tmp.push(at.get)}, tmp = []) || tmp.join('.'))+'.'+(to.path||[]).join('.')); return }
			var seen = as.seen || (as.seen = []), i = seen.length;
			while(i--){ if(d === (tmp = seen[i]).it){ v = d = tmp.link; break } }
		}
		if(k && v){ at.node = state_ify(at.node, k, s, d) } // handle soul later.
		else {
			if(!as.seen){ ran.err(as, "Data at root of graph must be a node (an object)."); return }
			as.seen.push(cat = {it: d, link: {}, todo: g? [] : Object.keys(d).sort().reverse(), path: (to.path||[]).slice(), up: at}); // Any perf reasons to CPU schedule this .keys( ?
			at.node = state_ify(at.node, k, s, cat.link);
			!g && cat.todo.length && to.push(cat);
			// ---------------
			var id = as.seen.length;
			(as.wait || (as.wait = {}))[id] = '';
			tmp = (cat.ref = (g? d : k? at.ref.get(k) : at.ref))._;
			(tmp = (d && (d._||'')['#']) || tmp.soul || tmp.link)? resolve({soul: tmp}) : cat.ref.get(resolve, {run: as.run, /*hatch: 0,*/ v2020:1, out:{get:{'.':' '}}}); // TODO: BUG! This should be resolve ONLY soul to prevent full data from being loaded. // Fixed now?
			//setTimeout(function(){ if(F){ return } console.log("I HAVE NOT BEEN CALLED!", path, id, cat.ref._.id, k) }, 9000); var F; // MAKE SURE TO ADD F = 1 below!
			function resolve(msg, eve){
				var end = cat.link['#'];
				if(eve){ eve.off(); eve.rid(msg) } // TODO: Too early! Check all peers ack not found.
				// TODO: BUG maybe? Make sure this does not pick up a link change wipe, that it uses the changign link instead.
				var soul = end || msg.soul || (tmp = (msg.$$||msg.$)._||'').soul || tmp.link || ((tmp = tmp.put||'')._||'')['#'] || tmp['#'] || (((tmp = msg.put||'') && msg.$$)? tmp['#'] : (tmp['=']||tmp[':']||'')['#']);
				!end && stun(as, msg.$);
				if(!soul && !at.link['#']){ // check soul link above us
					(at.wait || (at.wait = [])).push(function(){ resolve(msg, eve) }) // wait
					return;

View on GitHub (pinned to 552227599d)

Solutions

  1. Wrap the value in a node object: gun.get('soul').put({ field: value }) instead of putting the bare value
  2. Check the value at the call site — if it can be undefined/null/primitive, build the object explicitly
  3. For dynamic payloads, verify typeof value === 'object' before put and handle primitives separately
  4. Put the value under a key inside an existing node rather than at graph root

Example fix

// before
gun.get('settings').put('dark-mode');
// after
gun.get('settings').put({ theme: 'dark-mode' });
Defensive patterns

Strategy: validation

Validate before calling

function assertRootNode(value) {
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
    throw new Error('gun.put root must be a plain object node');
  }
}
assertRootNode(payload);

Type guard

const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;

Try / catch

gun.get('soul').put(payload, ack => {
  if (ack && String(ack.err).includes('root of graph')) {
    console.error('payload must be an object node, got:', typeof payload);
    return;
  }
});

Prevention

When it happens

Trigger: Calling gun.put('string'), gun.put(42), gun.put(true), gun.put(null) — a primitive at the root of a put; also gun.get(soul).put(primitive) where the walk starts at root without a key context, hitting the else branch with !as.seen.

Common situations: Migrating from key-value stores and putting bare values instead of node objects; writing a single field without wrapping it in an object; refactored code that lost the object wrapper; passing the result of a computation that unexpectedly became a primitive (or undefined/null).

Related errors


AI-assisted analysis of amark/gun@552227599d (2026-09-02). Data as JSON: /api/errors/a3e042e30ff98d2e. Report an issue: GitHub.