amark/gun · error

Invalid data: ${check(d)} at ${path}.${path}

Error message

Invalid data: ${check(d)} at ${path}.${path}

What it means

Gun's put validation (run in src/put.js) walks each value being written. If a value at some path is neither a valid primitive (valid(d)) nor a Gun graph node (Gun.is(d)) nor a plain object (Object.plain(d)), it is rejected as invalid data and err is raised with the offending value and its graph path. Gun only stores primitives, nodes, and plain objects — class instances, functions, undefined, Dates, etc. are not serializable graph data.

Source

Thrown at src/put.js:32

	var s = as.state = as.state || Gun.state();
	if('function' == typeof data){ data(function(d){ as.data = d; gun.put(u,u,as) }); return gun }
	if(!as.soul){ return get(as), gun }
	as.$ = root.$.get(as.soul); // TODO: This may not allow user chaining and similar?
	as.todo = [{it: as.data, ref: as.$}];
	as.turn = as.turn || turn;
	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.

View on GitHub (pinned to 552227599d)

Solutions

  1. Convert non-plain values to primitives before put: dates → ISO strings or epoch numbers
  2. Extract plain data from class instances/Maps/Sets (JSON round-trip or explicit toObject)
  3. Ensure undefined fields are removed or replaced with null/string placeholders
  4. For large shared objects, store them as their own node (soul) and link by reference instead of nesting

Example fix

// before
user.get('profile').put({ createdAt: new Date(), meta: someClassInstance });
// after
user.get('profile').put({
  createdAt: new Date().toISOString(),
  meta: JSON.parse(JSON.stringify(someClassInstance))
});
Defensive patterns

Strategy: validation

Validate before calling

function assertPutSafe(value, depth = 0) {
  if (value === null || ['string','number','boolean'].includes(typeof value)) return;
  if (depth > 5) throw new Error('too deep for gun put');
  const proto = Object.getPrototypeOf(value);
  if (value instanceof Date || typeof value === 'function' || value instanceof Map || value instanceof Set) {
    throw new Error('non-graph value: ' + value);
  }
  if (proto === Object.prototype || proto === null || Array.isArray(value)) {
    Object.values(value).forEach(v => assertPutSafe(v, depth + 1));
    return;
  }
  throw new Error('class instances are not plain objects: ' + value);
}

Type guard

const isPutSafeValue = (v) =>
  v === null || ['string','number','boolean'].includes(typeof v) ||
  (typeof v === 'object' && (v.constructor === Object || v instanceof Date === false) && Object.getPrototypeOf(v) === Object.prototype);

Try / catch

gun.get('node').put(data, ack => {
  if (ack && ack.err) {
    if (String(ack.err).startsWith('Invalid data:')) {
      console.error('gun rejected non-plain value at:', ack.err);
    }
    return;
  }
});

Prevention

When it happens

Trigger: gun.put() or gun.get(...).put(...) with a field value that is a Date, function, class instance, undefined, NaN or other non-plain object; deeply nested non-plain values discovered while walking the todo path (path shown as via-path + to-path in the message).

Common situations: Storing Date objects instead of timestamps; storing a Map/Set or class instance returned from an ORM; writing values still wrapped in a Proxy or reactive framework object; accidentally putting a function or undefined from an unset variable; Vue/React state objects passed straight into put.

Related errors


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