Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions src/oss/langgraph/fault-tolerance.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ When a node fails—from a slow external API, a transient network error, or an u
- [**Timeouts**](#timeouts) — cap how long a single attempt may run
- [**Error handling**](#error-handling) — run a recovery function after all retries are exhausted

Use [**`set_node_defaults`**](#graph-wide-defaults-with-set_node_defaults) to configure these mechanisms once for all nodes instead of repeating them on every `add_node` call.

These compose in a fixed order: when a node attempt raises any exception (including @[`NodeTimeoutError`] from a timeout), the retry policy decides whether to retry. Only after retries are exhausted does the error handler run.

For stopping a run cleanly at a superstep boundary and resuming later, see [Graceful shutdown](/oss/langgraph/durable-execution#graceful-shutdown).
Expand Down Expand Up @@ -424,6 +426,113 @@ Failure provenance is checkpointed. If the graph is interrupted or the process c

If a node wraps a subgraph and the subgraph raises an unhandled exception, that exception surfaces to the parent node. If the parent node has an `error_handler`, the handler fires with the subgraph's exception in `error.error`.

## Graph-wide defaults with `set_node_defaults`

<Note>
Requires `langgraph>=1.2`.
</Note>

Instead of repeating the same `retry_policy=`, `error_handler=`, `timeout=`, or `cache_policy=` on every `add_node` call, use `set_node_defaults()` to configure graph-wide defaults in one place:

```python
from langgraph.errors import NodeError
from langgraph.types import RetryPolicy, TimeoutPolicy
from langgraph.graph import StateGraph, START
from typing_extensions import TypedDict

class State(TypedDict):
status: str

def default_error_handler(state: State, error: NodeError) -> State:
return {"status": f"handled: {error.error}"}

graph = (
StateGraph(State)
.set_node_defaults(
retry_policy=RetryPolicy(max_attempts=3),
error_handler=default_error_handler,
timeout=TimeoutPolicy(run_timeout=30),
)
.add_node("step_a", step_a)
.add_node("step_b", step_b)
.add_edge(START, "step_a")
.compile()
)
```

Both `step_a` and `step_b` now share the same retry policy, error handler, and timeout without any duplication.

### Precedence

Per-node values passed directly to `add_node()` always override the defaults set by `set_node_defaults()`. Defaults are resolved at `compile()` time, so you can call `set_node_defaults()` before or after `add_node()` in any order:

```python
graph = (
StateGraph(State)
.set_node_defaults(error_handler=default_error_handler)
.add_node("step_a", step_a) # uses default_error_handler
.add_node("step_b", step_b, error_handler=custom_error_handler) # uses custom_error_handler
.add_edge(START, "step_a")
.compile()
)
```

### Default error handler

The `error_handler` default is particularly valuable when you want a single catch-all recovery function for any node that fails without its own handler. The handler accepts the same `(state, error: NodeError)` signature described in [Error handling](#error-handling):

```python
from langgraph.errors import NodeError
from langgraph.graph import StateGraph, START
from langgraph.types import RetryPolicy
from typing_extensions import TypedDict

class State(TypedDict):
status: str

def always_failing(state: State) -> State:
raise ValueError("something went wrong")

def default_handler(state: State, error: NodeError) -> State:
return {"status": f"recovered from {error.node}: {error.error}"}

graph = (
StateGraph(State)
.set_node_defaults(
retry_policy=RetryPolicy(max_attempts=2),
error_handler=default_handler,
)
.add_node("always_failing", always_failing)
.add_edge(START, "always_failing")
.compile()
)
```

The node is retried twice, then `default_handler` runs. The default handler also accepts `RunnableConfig` as an optional third argument if you need access to config values such as `thread_id`:

```python
from langchain_core.runnables import RunnableConfig

def default_handler(state: State, error: NodeError, config: RunnableConfig) -> State:
thread_id = config["configurable"].get("thread_id")
return {"status": f"handled on thread {thread_id}"}
```

### Applicability matrix

Not all defaults apply to all node types. Error-handler nodes (those registered via `add_node(error_handler=...)`) are excluded from certain defaults to prevent unsafe behavior:

| `set_node_defaults` parameter | Applies to regular nodes | Applies to error-handler nodes | Reason |
| ----------------------------- | ------------------------ | ------------------------------ | ------ |
| `retry_policy` | ✅ | ✅ | Handlers should be retried on transient failures |
| `timeout` | ✅ | ✅ | Stuck handlers should be cancelled like stuck regular nodes |
| `error_handler` | ✅ | ❌ | Handlers must never catch themselves |
| `cache_policy` | ✅ | ❌ | Caching handler results is unsafe |

### Scope

Defaults set on a parent graph are **not** inherited by subgraphs. Each graph maintains its own defaults.

## Functional API

The same `timeout=` and `retry_policy=` parameters are available on `@task` and `@entrypoint` in the functional API:
Expand Down Expand Up @@ -454,5 +563,6 @@ The behavior is identical to `add_node`: `NodeTimeoutError` is raised on timeout
- **Timeouts are async-only**: sync nodes with a `timeout` are rejected at compile time.
- **One handler per node**: each node can have at most one `error_handler`.
- **Handler failures bubble up**: if the error handler itself raises, that exception propagates as if the node had no handler.
- **`set_node_defaults` is not inherited by subgraphs**: each graph manages its own defaults independently.

:::
Loading