clockworklabs/SpacetimeDB · error · TypeError

could not serialize result: object had neither a `ok` nor an

Error message

could not serialize result: object had neither a `ok` nor an `err` field

What it means

Sum types whose two variants are named exactly 'ok' (index 0) and 'err' (index 1) get a specialized serializer: it writes discriminant byte 0/1 plus the payload field. At serialize time it probes the value with 'ok' in value / 'err' in value; a value with neither field (null, {}, arrays, or mis-cased keys like Ok/Err) throws this TypeError.

Source

Thrown at crates/bindings-typescript/src/lib/algebraic_type.ts:672

    ) {
      const serializeOk = AlgebraicType.makeSerializer(
        ty.variants[0].algebraicType,
        typespace
      );
      const serializeErr = AlgebraicType.makeSerializer(
        ty.variants[0].algebraicType,
        typespace
      );

      return (writer, value) => {
        if ('ok' in value) {
          writer.writeU8(0);
          serializeOk(writer, value.ok);
        } else if ('err' in value) {
          writer.writeU8(1);
          serializeErr(writer, value.err);
        } else {
          throw new TypeError(
            'could not serialize result: object had neither a `ok` nor an `err` field'
          );
        }
      };
    } else {
      let serializer = SERIALIZERS.get(ty);
      if (serializer != null) return serializer;

      const serializers: Record<string, Serializer<any>> = {};

      const body = `\
switch (value.tag) {
${ty.variants
  .map(
    ({ name }, i) => `\
  case ${JSON.stringify(name!)}:
    writer.writeByte(${i});
    return this.${name!}(writer, value.value);`

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Wrap the payload: serialize { ok: value } or { err: value }
  2. Verify the variant names in the module schema are exactly ok/err (the bindings special-case those spellings)
  3. Type-check values before insert/reducer calls so unwrapped or null payloads fail in your code, not inside the serializer

Example fix

// before
reducers.set_result(payload); // payload = 'done' or undefined -> throws

// after
reducers.set_result({ ok: payload });
// or
reducers.set_result({ err: 'not permitted' });
Defensive patterns

Strategy: type-guard

Validate before calling

function isResultLike(v: unknown): boolean {
  return typeof v === 'object' && v !== null && ('ok' in v || 'err' in v);
}
// if (!isResultLike(value)) throw new TypeError('expected { ok } or { err }');

Type guard

type ResultLike<T = unknown, E = unknown> = { ok: T } | { err: E };
function isResultLike<T, E>(v: unknown): v is ResultLike<T, E> {
  return typeof v === 'object' && v !== null && ('ok' in v || 'err' in v);
}

Prevention

When it happens

Trigger: Serializing a Result-shaped sum value that is null/undefined, {}, uses different casing ({ Ok: x } or { Err: e }), or passes the payload unwrapped (value instead of { ok: value }) - e.g. inserting into a Result-typed column or calling a reducer with a Result argument.

Common situations: Module schema declares the variants with different casing than the TS bindings expect (exactly 'ok'/'err'); JSON payloads where the wrapper object was dropped; writing a default/undefined value to a Result column.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/f948691288947a28. Report an issue: GitHub.