reflex-dev/reflex · error · EventHandlerTypeError

Arguments to event handlers must be Vars or JSON-serializabl

Error message

Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}.

What it means

Event-handler arguments become part of the JSON event payload sent to the browser; anything that is not a Var or JSON-serializable cannot be represented and the LiteralVar.create call raises TypeError, which is wrapped in this EventHandlerTypeError.

Source

Thrown at packages/reflex-base/src/reflex_base/event/__init__.py:669

            # Special case for file uploads. The upload arg takes its own
            # positional slot so the remaining args stay aligned with fn_args,
            # but its parameter name is re-derived server-side in as_event_spec.
            if isinstance(arg, (FileUpload, UploadFilesChunk)):
                if upload_event_spec is not None:
                    msg = (
                        f"Event handler {self.fn.__name__} received multiple file "
                        "upload arguments."
                    )
                    raise EventHandlerTypeError(msg)
                upload_event_spec = arg.as_event_spec(handler=self)
                continue

            # Otherwise, convert to JSON.
            try:
                payload.append((Var(_js_expr=fn_arg), LiteralVar.create(arg)))
            except TypeError as e:
                msg = f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
                raise EventHandlerTypeError(msg) from e

        if upload_event_spec is not None:
            if not payload:
                return upload_event_spec
            # The extra bound args share the flat payload with the synthetic
            # upload args, so reject names that would clobber a reserved upload
            # key (files, upload_id, extra_headers, ...).
            payload_names = [name._js_expr for name, _ in payload]
            reserved = {name._js_expr for name, _ in upload_event_spec.args}
            clash = next((name for name in payload_names if name in reserved), None)
            if clash is not None:
                msg = (
                    f"Event handler {self.fn.__name__} argument {clash!r} conflicts "
                    "with a reserved upload argument."
                )
                raise EventHandlerTypeError(msg)
            # The client uploadFiles handler forwards exactly the args named here,
            # so it never has to know the reserved upload keys.

View on GitHub (pinned to 45b8ed5ab7)

Solutions

  1. Convert the value to primitives (str/int/float/bool/list/dict) or its JSON equivalent before passing
  2. Wrap the value in a Var if it is already a computed Var
  3. Pass an id/key and look the object up server-side in the handler instead

Example fix

# before
State.select(user)  # user is a SQLAlchemy model
# after
State.select(user.id)
Defensive patterns

Strategy: type-guard

Validate before calling

import json

def is_event_serializable(v) -> bool:
    try:
        json.dumps(v, default=lambda o: getattr(o, '_var_type', None) or (_ for _ in ()).throw(TypeError))
        return True
    except TypeError:
        return hasattr(v, '_var_type')  # it's a Var

Type guard

def is_event_arg(v) -> TypeGuard[object]: import json; try: json.dumps(v); return True; except TypeError: return hasattr(v, '_var_type')

Try / catch

try:
    State.h(obj)
except EventHandlerTypeError:
    State.h(obj.id)  # fallback to identifier

Prevention

When it happens

Trigger: Calling State.my_event(obj) where obj is e.g. a database model, socket, PIL image, set, or custom class without a serializer.

Common situations: Passing ORM objects or numpy arrays from render code; storing non-serializable values and forwarding them to events; forgetting to extract primitive fields first.

Related errors


AI-assisted analysis of reflex-dev/reflex@45b8ed5ab7 (2026-08-28). Data as JSON: /api/errors/5bd6bf321a90379f. Report an issue: GitHub.