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
- Wrap the value in a node object: gun.get('soul').put({ field: value }) instead of putting the bare value
- Check the value at the call site — if it can be undefined/null/primitive, build the object explicitly
- For dynamic payloads, verify typeof value === 'object' before put and handle primitives separately
- 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
- Always put node objects, never bare primitives
- Wrap single values under a key: put({ field: value })
- Guard dynamic payloads: check typeof payload === 'object' before put
- Watch for variables that may be undefined/null at call time
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
- Invalid data: ${check(d)} at ${path}.${path}
- Your user account is not published for dApps to access, plea
- Wrong user or password.
- User cannot be found!
AI-assisted analysis of amark/gun@552227599d (2026-09-02).
Data as JSON: /api/errors/a3e042e30ff98d2e.
Report an issue: GitHub.