For most of LangGraph's life, a node that failed had exactly one honest option: raise, and let the graph die. You could bolt on a RetryPolicy to try again, but once the budget was gone the exception propagated to the top and your run was over — half its side effects already committed, nothing to undo them. Agent developers papered over this with try/except inside the node function, which works right up until the recovery logic needs to touch graph state or route somewhere else, at which point you're reimplementing the router by hand.

LangGraph 1.2 finally splits failure into first-class primitives. There are now three of them, and the reason this piece exists is that they look interchangeable and do genuinely opposite things to your persisted state. Pick the wrong one and your compensation logic runs on a lie.

Three ways to stop a node#

All three attach to the node, next to the logic they protect, via add_node:

Line those up and the tell is what each does to the work the node already did. A timeout destroys the attempt's writes. A drain preserves them and checkpoints. The error handler sits in between: the failed attempt's writes are gone, but you're handed the wheel to write whatever compensating state you want before routing on.

Why the distinction is the whole point#

Here's the failure mode nobody warns you about. Say you have a charge_payment node followed by reserve_inventory, and inventory fails. The instinct is to reach for the tool that "handles the error." So you set a timeout on reserve_inventory, watch it fire, and feel covered. But a timeout just clears that node's writes and retries — it knows nothing about the payment you already committed one superstep ago. The money is gone and no code you wrote will ever refund it, because the primitive that fired is designed to forget, not to compensate.

The Saga pattern is the correct model for this, and it's the model error_handler was built for:

def inventory_error_handler(state: State, error: NodeError) -> Command:
    # the forward action failed; run the compensating action
    return Command(
        update={"status": f"refunded: {error.error}"},
        goto="refund_payment",
    )

builder.add_node(
    "reserve_inventory", reserve_inventory,
    retry=RetryPolicy(max_attempts=3),
    error_handler=inventory_error_handler,
)

The handler doesn't fire until retries are spent, so you're not compensating for a transient blip — you're compensating for a real, durable failure. And because it returns a Command, the recovery is a normal graph edge: it routes to refund_payment, which is itself just a node, with its own retry policy and its own error handler if the refund can fail too. That recursion is the thing try/except could never give you cleanly. Compensation becomes part of the graph topology instead of a pile of nested exception blocks that can't see the router.

A timeout forgets the attempt. A drain preserves it. An error handler is the only one of the three that lets you make it right — which is exactly why it's the one people reach for last.

The drain is not error handling#

The last trap: treating request_drain() as a fault primitive. It isn't. Drain is for orderly shutdown — an orchestrator reclaiming a pod, a deploy rolling nodes — where the run hasn't failed at all and you want to freeze it cleanly and resume elsewhere. It's cooperative and operates between supersteps, so it never interrupts a node that's mid-call. If you wire a SIGTERM handler to request_drain, a redeploy pauses your agents at a checkpoint boundary and they wake up on the new pod exactly where they left off. Use it for a timeout — expecting it to kill a wedged node — and you'll wait forever, because a genuinely hung node never yields the superstep for the drain to slip in.

Three primitives, three contracts with your state: destroy, preserve, compensate. LangGraph 1.2 stopped making you choose between "retry" and "crash," but it didn't make the choice for you. The framework will now do exactly what you asked — which means the bug is no longer a missing feature. It's asking the wrong one.