{"record":{"id":"c620e70cd9123b3e","repo":"apache/beam","slug":"on-timer-decorator-expected-timerspec","errorCode":null,"errorMessage":"@on_timer decorator expected TimerSpec.","messagePattern":"@on_timer decorator expected TimerSpec\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"sdks/python/apache_beam/transforms/userstate.py","lineNumber":233,"sourceCode":"            coders._TimerCoder(key_coder, window_coder)))\n\n\ndef on_timer(timer_spec: TimerSpec) -> Callable[[CallableT], CallableT]:\n  \"\"\"Decorator for timer firing DoFn method.\n\n  This decorator allows a user to specify an on_timer processing method\n  in a stateful DoFn.  Sample usage::\n\n    class MyDoFn(DoFn):\n      TIMER_SPEC = TimerSpec('timer', TimeDomain.WATERMARK)\n\n      @on_timer(TIMER_SPEC)\n      def my_timer_expiry_callback(self):\n        logging.info('Timer expired!')\n  \"\"\"\n\n  if not isinstance(timer_spec, TimerSpec):\n    raise ValueError('@on_timer decorator expected TimerSpec.')\n\n  def _inner(method: CallableT) -> CallableT:\n    if not callable(method):\n      raise ValueError('@on_timer decorator expected callable.')\n    if timer_spec._attached_callback:\n      raise ValueError(\n          'Multiple on_timer callbacks registered for %r.' % timer_spec)\n    timer_spec._attached_callback = method\n    return method\n\n  return _inner\n\n\ndef get_dofn_specs(dofn: 'DoFn') -> tuple[set[StateSpec], set[TimerSpec]]:\n  \"\"\"Gets the state and timer specs for a DoFn, if any.\n\n  Args:\n    dofn (apache_beam.transforms.core.DoFn): The DoFn instance to introspect for","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/python/apache_beam/transforms/userstate.py#L215-L251","documentation":"The @on_timer decorator requires its first argument to be a TimerSpec instance, because it attaches the decorated method to that spec's _attached_callback slot. Passing anything else (a string name, a StateSpec, None) is rejected with a ValueError at decoration time.","triggerScenarios":"Writing @on_timer('my_timer') with the timer's name string instead of the TimerSpec object; passing a StateSpec or custom object; forgetting to define the TimerSpec constant being referenced.","commonSituations":"Confusing @on_timer with stateful DoFn parameter injection where timers are passed by TimerSpec via @stateful; copy-pasting examples and replacing the spec constant with a name; older Beam examples using string-based timer references.","solutions":["Pass the TimerSpec instance: @on_timer(MY_TIMER_SPEC) where MY_TIMER_SPEC = TimerSpec('my_timer', TimeDomain.WATERMARK)","Define the TimerSpec at class level first and reference the object, not its name string","Ensure the spec argument is not a StateSpec or other spec type"],"exampleFix":"// before\n@on_timer('my_timer')\ndef expiry(self): ...\n// after\nTIMER_SPEC = TimerSpec('my_timer', TimeDomain.WATERMARK)\n@on_timer(TIMER_SPEC)\ndef expiry(self): ...","handlingStrategy":"validation","validationCode":"assert isinstance(TIMER_SPEC, TimerSpec), 'on_timer requires a TimerSpec instance'","typeGuard":"from apache_beam.transforms.userstate import TimerSpec\ndef is_timer_spec(x) -> bool:\n    return isinstance(x, TimerSpec)","tryCatchPattern":"try:\n    _ = MyDoFn()  # decoration errors surface at class-definition/import time\nexcept ValueError as e:\n    if 'on_timer' in str(e):\n        fix_timer_decorator()","preventionTips":["Reference the TimerSpec object, never its name string, in @on_timer(...)","Define TimerSpec constants at class level before the decorated methods","Add an import-time smoke test that instantiates stateful DoFns to catch decorator mistakes early"],"tags":["python","apache-beam","timers","decorator","invalid-argument"],"backgroundTag":"invalid-argument-value","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-20T03:17:13.778Z"}