Foundations

Serialization

Understand how values cross between the host and the sandbox.

Serialization

The host and guest do not share JavaScript memory. Every value that crosses the sandbox boundary must be encoded, copied, and reconstructed on the receiving side.

Serialization is used for host function arguments, host function outputs, final run results, interruption payloads, resolutions, and the host function history stored in a continuation.

Values

The serialization format supports JavaScript values that cannot be represented by plain JSON. In addition to strings, numbers, booleans, null values, objects, and arrays, it supports undefined, BigInt, NaN, infinity, and negative zero.

It also supports dates, regular expressions, maps, sets, array buffers, data views, typed arrays, errors, and aggregate errors. Cyclic objects, sparse arrays, and repeated references can be represented within a single transferred value.

For example, a host function can return a map containing a date:

const hostFunctions = {
  reports: {
    load: () =>
      new Map([
        ['generatedAt', new Date()],
        ['total', 42],
      ]),
  },
};

Functions, symbols, promises, weak collections, weak references, and arbitrary class instances cannot be transferred as values. A host function may return a promise because the runtime awaits it, but the promise itself is not sent to the guest.

Do not return objects that contain functions or depend on class behavior:

const hostFunctions = {
  tasks: {
    get: () => ({
      id: 'task_123',
      title: 'Publish documentation',
      complete: () => {
        // Update the task.
      },
    }),
  },
};

This value cannot be serialized because complete is a function. Expose the operation as a separate host function and return only data:

const hostFunctions = {
  tasks: {
    get: () => ({
      id: 'task_123',
      title: 'Publish documentation',
    }),
    complete: async (id: string) => {
      await completeTask(id);
      return { completed: true };
    },
  },
};

Similarly, a host function cannot return an application-specific class instance directly:

const hostFunctions = {
  accounts: {
    get: async (id: string) => {
      // This fails if loadAccount returns an Account instance.
      return loadAccount(id);
    },
  },
};

The serializer cannot transfer the instance's custom prototype, methods, or private state. The host function must select the required properties and return an explicit data object instead:

const hostFunctions = {
  accounts: {
    get: async (id: string) => {
      const account = await loadAccount(id);

      return {
        id: account.id,
        balance: account.balance,
      };
    },
  },
};

TypeScript types describe the expected shape to host developers, but they do not validate guest values at runtime. Host functions must validate untrusted arguments before using them.

Identity

Object identity is preserved within one transferred value. If two properties refer to the same object before serialization, they refer to the same reconstructed object after deserialization.

const shared = { value: 42 };

return {
  first: shared,
  second: shared,
};

Identity is not preserved across separate host function calls. Returning the same host object twice creates a separate guest copy each time. Mutating a guest copy also does not mutate the original host object.

Application-specific prototypes and class methods are not reconstructed. Host functions should exchange data rather than objects whose behavior depends on a custom prototype.

Errors

An error transferred as ordinary data can retain its name, message, cause, and aggregate errors. Its original stack is not transferred as data.

A thrown host function error follows a different path. The runtime converts it into a sanitized guest-side rejection so that host stack traces and private diagnostics are not exposed inside the sandbox.

try {
  return await payments.charge(500);
} catch (error) {
  return {
    charged: false,
    message: error.message,
  };
}

If guest code does not catch the rejection, the entire invocation fails.

Limits

Serialization limits are measured using the UTF-8 byte length of the encoded value. The default limit is 1 MiB for the arguments of one host function call, 4 MiB for one host function output, and 1 MiB for the final run result.

Console output has a shared 64 KiB limit for each invocation. Continuation state has a separate 32 MiB limit, and source code has a 256 KiB limit before execution begins.

A value that appears small as a JavaScript object can have a much larger serialized representation. Return references, summaries, or paginated data rather than whole record sets.

Contracts

Treat the serialization boundary like an API boundary. Give each host function an explicit input and output shape, and do not rely on host implementation details on either side.

This host function validates a small input object and returns only the fields the guest needs:

const hostFunctions = {
  search: {
    query: async (input: { query: string; limit: number }) => {
      if (
        typeof input.query !== 'string' ||
        !Number.isInteger(input.limit) ||
        input.limit < 1 ||
        input.limit > 20
      ) {
        throw new TypeError('Invalid search input.');
      }

      const matches = await searchIndex.query(input.query, input.limit);

      return matches.map(match => ({
        id: match.id,
        title: match.title,
      }));
    },
  },
};