#40878·langchain

`with_fallbacks(exception_key=...)` writes the exception into the caller's own input mapping Package:langchain-core

Author: Iris070308Created Sep 28, 2026Updated Sep 28, 2026
Labelsbugcoreexternal

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)

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)

bash
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