`with_fallbacks(exception_key=...)` writes the exception into the caller's own input mapping Package:langchain-core
Submission checklist
- This is a bug, not a usage question.
- I added a clear and descriptive title that summarizes this issue.
- I used the GitHub search to find a similar question and didn't find it.
- I am sure that this is a bug in LangChain rather than my code.
- The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).
- This is not related to the langchain-community package.
- I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.
Package (Required)
- langchain
- langchain-openai
- langchain-anthropic
- langchain-classic
- langchain-core
- langchain-model-profiles
- langchain-tests
- langchain-text-splitters
- langchain-chroma
- langchain-deepseek
- langchain-exa
- langchain-fireworks
- langchain-groq
- langchain-huggingface
- langchain-mistralai
- langchain-nomic
- langchain-ollama
- langchain-openrouter
- langchain-perplexity
- langchain-qdrant
- langchain-typesafe
- langchain-xai
- Other / not sure / general
Related Issues / PRs
No response
Reproduction Steps / Example Code (Python)
"""Repro: with_fallbacks(exception_key=...) writes the exception into the caller's own input mapping.
The mapping handed to the fallback should carry the key; the dict you passed in should not be touched.
Exit code 1 means the caller's dict was modified.
"""
import asyncio
from langchain_core.runnables import RunnableLambda
def boom(_):
raise ValueError("boom")
chain = RunnableLambda(boom).with_fallbacks(
[RunnableLambda(lambda value: "fallback")],exception_key="error"
)
results = {}
def fresh():
return {"a": 1}
for name, call in (("invoke", chain.invoke),
("stream", lambda plist(chain.stream(p))),
("batch", lambda p: chain.batch([p]))):
payload = fresh()
call(payload)
results[name] = sorted(payload)
async def run_async():
payload = fresh()
await chain.ainvoke(payload)
results["ainvoke"] = sorted(payload)
payload = fresh()
async for _ in chain.astream(payload):
pass
results["astream"] = sorted(payload)
payload = fresh()
await chain.abatch([payload])
results["abatch"] = sorted(payload)
asyncio.run(run_async())
mutated = []
for name in ("invoke", "ainvoke", "batch", "abatch", "stream", "astream"):
keys = results[name]
if keys != ["a"]:
mutated.append(name)
print(f" {name:8s} caller keys after the call: {keys}{' <-- MUTATED' if keys != ['a'] else ''}")
print(f"\nFAIL: the caller's mapping was written to by {mutated}" if mutated else "\nOK: no caller mapping was modified")
raise SystemExit(1 if mutated else 0)Error Message and Stack Trace (if applicable)
invoke caller keys after the call: ['a', 'error'] <-- MUTATED
ainvoke caller keys after the call: ['a', 'error'] <-- MUTATED
batch caller keys after the call: ['a', 'error'] <-- MUTATED
abatch caller keys after the call: ['a', 'error'] <-- MUTATED
stream caller keys after the call: ['a', 'error'] <-- MUTATED
astream caller keys after the call: ['a', 'error'] <-- MUTATED
FAIL: the caller's mapping was written to by ['invoke', 'ainvoke', 'batch', 'abatch', 'stream', 'astream']Description
with_fallbacks(..., exception_key="error") writes the exception into the dict I passed in. The docstring says handled exceptions are passed to fallbacks "as part of the input under the specified key", which I read as the mapping handed to the fallback carrying that key -- my own dict is not part of that. But after the call {"a": 1} is {"a": 1, "error": ValueError('boom')}, and all six entry points do it: fallbacks.py lines 189 (invoke), 243 (ainvoke), 340 (batch), 439 (abatch), 492 (stream), 556 (astream). Each one assigns into the input before entering the fallback.
It bit me because I reuse one input dict across a few chains and log it, and an exception object travelling along inside it is not something I want in logs. The return value is fine; it is the input that gets modified.
Copying first looks like enough, e.g. input_ = {**input, self.exception_key: last_error}.
System Info
langchain-core: 1.6.5 langchain: 1.4.2 langgraph: 1.2.11 pydantic: 2.12.5 Python: 3.11.15 upstream commit tested: 1ef23d6b7f6d83508a8cb531feeb88860e4dec40 OS: Windows 10 + WSL No API key or network access is needed for the reproduction.
Social handles (optional)
No response
Source: langchain-ai/langchain