denoland/deno · error

Unable to deserialize result parameter.

Error message

Unable to deserialize result parameter.

What it means

For Deno FFI callbacks (Deno.UnsafeCallback) whose native result type is a pointer or buffer, the value returned by the JS callback must be a typed array or ArrayBuffer so its data pointer can be copied to the native caller. If the callback returns anything else for such a result type (undefined, null, plain object, string), the FFI bridge panics with this message. The panic happens across the native boundary, so a JS try/catch cannot intercept it.

Source

Thrown at ext/ffi/callback.rs:551

        {
          let byte_offset = value.byte_offset();
          let ab = value
            .buffer(scope)
            .expect("Unable to deserialize result parameter.");
          size = value.byte_length();
          ab.data()
            .expect("Unable to deserialize result parameter.")
            .as_ptr()
            .add(byte_offset)
        } else if let Ok(value) = v8::Local::<v8::ArrayBuffer>::try_from(value)
        {
          size = value.byte_length();
          value
            .data()
            .expect("Unable to deserialize result parameter.")
            .as_ptr()
        } else {
          panic!("Unable to deserialize result parameter.");
        };
        std::ptr::copy_nonoverlapping(
          pointer as *mut u8,
          result as *mut u8,
          std::cmp::min(size, (*cif.rtype).size),
        );
      }
      NativeType::Void => {
        // nop
      }
    };
  }
}

#[op2]
pub fn op_ffi_unsafe_callback_ref(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: ResourceId,

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Make the callback return a Uint8Array (or matching TypedArray) for buffer results on every code path, including error branches
  2. Use `result: "pointer"` with a BigInt address only when you truly manage that memory yourself
  3. Return `new Uint8Array(0)` instead of null when there is no data
  4. Cover every callback branch in tests; the panicking path is the one you did not test

Example fix

// before — buffer result, but the error branch returns undefined
const cb = new Deno.UnsafeCallback(
  { parameters: ["i32"], result: "buffer" },
  (code: number) => code > 0 ? new Uint8Array([code]) : null,
);

// after — every path returns a TypedArray
const cb = new Deno.UnsafeCallback(
  { parameters: ["i32"], result: "buffer" },
  (code: number) => new Uint8Array([code & 0xff]),
);
Defensive patterns

Strategy: type-guard

Validate before calling

const cb = new Deno.UnsafeCallback(
  { parameters: [], result: "buffer" },
  () => {
    const v = compute();
    return v instanceof Uint8Array ? v : new Uint8Array(0); // never panic the bridge
  },
);

Type guard

function isFFIBufferResult(v: unknown): v is Uint8Array | ArrayBuffer {
  return v instanceof ArrayBuffer ||
    (ArrayBuffer.isView(v) && !(v instanceof DataView));
}

Prevention

When it happens

Trigger: A Deno.UnsafeCallback declared with result: "buffer" (or another pointer-backed result type) whose JS function returns undefined/null/a plain value on at least one code path; returning a number is reinterpreted as a raw pointer, which is usually a second bug waiting to happen.

Common situations: Porting C callback contracts to Deno FFI; error branches in the JS callback that forget to return a buffer; refactors changing what the callback returns while the FFI signature stays the same.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/d4c3d79af0c49d28. Report an issue: GitHub.