Error Handling

Catch and recover from agent, tool, and LLM errors

Structured exceptions tell you what failed, whether to retry, and which agent or run was involved — without parsing raw tracebacks.

python
from praisonaiagents import Agent, PraisonAIError

agent = Agent(name="assistant", instructions="Be helpful")

try:
    print(agent.start("Say hello in one sentence."))
except PraisonAIError as e:
    print(f"{e.error_category}: {e.message} (agent={e.agent_id}, run={e.run_id})")

The user runs the agent; failures raise typed PraisonAIError with category, message, and run context for recovery.

Diagram

praisonaiagents.PraisonAIError — one base for all

🛡 PraisonAIError

🧠 Core SDK errors
ToolExecutionError
LLMError
ValidationError
NetworkError

📦 Wrapper errors
RecipeError, CLIExecutionError,
JobQueueFull, ToolTimeoutError, …

Quick Start

  1. Catch any agent error
    python
    from praisonaiagents import Agent, PraisonAIError
    
    agent = Agent(name="assistant", instructions="Be helpful")
    
    try:
        result = agent.start("Say hello in one sentence.")
        print(result)
    except PraisonAIError as e:
        print(f"{e.error_category}: {e.message} (agent={e.agent_id}, run={e.run_id})")
  2. Handle tool errors specifically
    python
    from praisonaiagents import Agent, tool, ToolExecutionError
    
    @tool
    def divide(a: float, b: float) -> float:
        """Divide two numbers."""
        return a / b
    
    agent = Agent(instructions="Use divide.", tools=[divide])
    
    try:
        agent.start("Divide 10 by 0.")
    except ToolExecutionError as e:
        print(f"Tool {e.tool_name} failed: {e.message}")
        print(f"Retryable: {e.is_retryable}")
  3. Catch a failed LLM tool-calling loop
    python
    from praisonaiagents import Agent
    from praisonaiagents.llm import LLMResponseError
    
    agent = Agent(name="assistant", instructions="Answer clearly.")
    
    try:
        result = agent.start("Summarise the latest project update.")
    except LLMResponseError as e:
        # A mid-loop tool-calling error now raises instead of returning ""
        print(f"LLM call failed: {e.message}")

How It Works

Diagram
🔧 Tool🧠 Agent👤 User🔧 Tool🧠 Agent👤 Userpromptcall toolraises ToolExecutionErrorexception propagates (unless caught)

Every structured error carries message, agent_id, run_id, error_category, and is_retryable. Subclasses add domain fields such as tool_name or model_name.

ClassWhen raisedKey fieldsDefault retryable
PraisonAIErrorBase — catch-allerror_category, contextFalse
ToolExecutionErrorTool fails or loop-guard HALTtool_nameTrue
LLMErrorChat completion failsmodel_nameFalse
ValidationErrorInvalid config or inputfield_nameFalse
NetworkErrorExternal service unreachableservice_name, status_codeTrue
LLMResponseErrorLLM tool-calling loop fails mid-iteration__cause__ (chained)False

error_category uses typed kinds such as rate_limit, auth, context_overflow, and billing.

LLMResponseError is raised by LLM.get_response() when the tool-calling loop fails and cannot produce a response — previously this was swallowed and returned as an empty string. Catch it to distinguish tool-loop failures; a try/except Exception already covers it. See LLMResponseError.


LLM Response Errors

LLMResponseError is raised when the LLM tool-calling loop hits an exception it cannot recover from. It lives in praisonaiagents.llm alongside the other LLM exceptions:

python
from praisonaiagents.llm import LLMResponseError
ExceptionModuleRaised when
LLMResponseErrorpraisonaiagents.llmThe tool-calling loop in LLM.get_response() fails mid-iteration and surfaces to the caller
LLMContextLengthExceededExceptionpraisonaiagents.llmThe prompt exceeds the model’s context window

LLMResponseError carries a message attribute describing the iteration that failed and chains the original exception via raise … from e, so e.__cause__ holds the underlying error.


Wrapper exceptions (praisonai.*)

PraisonAIError now catches every root exception in the praisonai wrapper too, so one except handler covers both tiers.

python
from praisonaiagents import PraisonAIError
from praisonai.recipe.registry import LocalRegistry

try:
    LocalRegistry().pull("missing-recipe")
except PraisonAIError as e:
    # Also catches RecipeError, CLIExecutionError, JobQueueFull, ToolTimeoutError, …
    print(f"{type(e).__name__}: {e}")

Stdlib parents stay in place through multiple inheritance, so existing except ValueError, except RuntimeError, and except TimeoutError handlers keep matching. Keep importing the base from praisonaiagents — the wrapper adds no new public module.

ExceptionModuleStdlib parent (still matches)
ToolTimeoutErrorpraisonai.agents_generatorTimeoutError
AgentConfigErrorpraisonai.api.agent_invokeValueError
MergeConflictErrorpraisonai.auto—
LangfuseAPIErrorpraisonai.cli.langfuse_client—
CLIExecutionErrorpraisonai.integrations.baseRuntimeError
ManagedSandboxRequiredpraisonai.integrations.managed_agentsRuntimeError
JobQueueFullpraisonai.jobs.executor—
RecipeErrorpraisonai.recipe.exceptions—
RunHistoryErrorpraisonai.recipe.history—
PolicyErrorpraisonai.recipe.policy—
RegistryErrorpraisonai.recipe.registry—
SecurityErrorpraisonai.recipe.security—
RerunErrorpraisonai.replay.rerunRuntimeError
StrictModeErrorpraisonai.templates.dependency_checker—
SecurityErrorpraisonai.templates.tool_override—

Subclasses such as RecipeNotFoundError, RunNotFoundError, and PolicyDeniedError inherit the change automatically.

Diagram

Yes

No

Tool call failed

LLM failed

CLI integration failed

Recipe / registry / policy

Job queue saturated

🤔 What do you want to handle?

Any PraisonAI
failure at all?

except PraisonAIError

Specific category?

except ToolExecutionError

except LLMError or
LLMResponseError

except CLIExecutionError

except RecipeError /
RegistryError / PolicyError

except JobQueueFull


Observing a terminal LLM failure

chat() / achat() return None on an unrecoverable LLM failure (a 500, a revoked key, a 429 after all retries). As of PR #5764, agent.last_stop_reason is set to "error" in that same path, so callers that cannot catch the raised exception (web handlers, bots, batch jobs) can still tell “the model answered empty” from “the model never answered”.

python
from praisonaiagents import Agent

agent = Agent(name="Assistant", instructions="Be helpful")

reply = agent.chat("Draft a reply")
if reply is None and agent.last_stop_reason == "error":
    # The LLM call failed terminally. Retry on a healthy endpoint,
    # or surface the failure to the user.
    print("The model could not answer — please try again.")

The reason is reset to "completed" at the start of every turn, so an "error" from a previous turn cannot leak into the next one.


Tool Failure Behavior

On both chat() and achat(), a tool failure resolves down one of three branches:

  1. Tool raises an exception → the framework wraps it as ToolExecutionError and propagates it.
  2. Tool returns {"error": "..."} → the error dict is handed back to the LLM as a normal tool result, so the model can self-correct (new behavior since PR #4462, confirmed in #4470 which fixes issue #4446. The generic-exception branch in openai_client.py behaves the same way.).
  3. Tool returns a retryable-transient / denial dict → ToolExecutionError with is_retryable=True, or a short-circuit denial.
Diagram

Tool failure — sync (chat) and async (achat)

raises exception

returns error dict

retryable / denied

👤 chat / achat request

🤖 Model calls tool

🔧 Tool result

🛑 ToolExecutionError propagates

📄 Error dict returned to LLM
LLM self-corrects

🔁 ToolExecutionError
is_retryable / denial break

If your tool raises, your agent’s turn ends — the same way whether you call chat() or achat(). Wrap both in try / except ToolExecutionError:

python
import asyncio
from praisonaiagents import Agent, tool, ToolExecutionError

@tool
def fetch(url: str) -> str:
    """Fetch a URL."""
    raise RuntimeError("429 too many requests")

agent = Agent(instructions="Use fetch.", tools=[fetch])

try:
    agent.start("Fetch https://example.com")
except ToolExecutionError as e:
    print(f"Tool {e.tool_name} failed: {e.message}")

# Async — raises the same ToolExecutionError
try:
    asyncio.run(agent.achat("Fetch https://example.com"))
except ToolExecutionError as e:
    print(f"Tool {e.tool_name} failed: {e.message}")

A tool that returns an error dict (instead of raising) no longer aborts the run — the dict flows back to the model, which retries with corrected arguments:

python
from praisonaiagents import Agent, tool

@tool
def fetch(url: str) -> dict:
    """Fetch a URL — returns an error dict on bad input."""
    if not url:
        return {"error": "Empty URL", "stdout": "", "stderr": ""}
    ...

agent = Agent(instructions="Use fetch.", tools=[fetch])
# The model calling fetch("") no longer aborts the run.
# The error dict flows back to the model and it retries with a real URL.
agent.start("Fetch example.com and summarise.")

Sync and async paths are aligned (PR #6109). A tool that raises ends the turn with ToolExecutionError on both chat() and achat() — previously achat() handed the error to the model and returned its “the operation failed” reply as a success, so side-effecting tools (email, payments) could fail silently. A tool that returns {"error": "..."} is handed to the LLM as a normal tool result on both. See Async Tool Safety.


Common Patterns

Retry on transient network failures, fail on config bugs:

python
from praisonaiagents import Agent, NetworkError, ValidationError

agent = Agent(name="assistant")

for attempt in range(3):
    try:
        print(agent.start("Summarise today's news."))
        break
    except NetworkError:
        if attempt == 2:
            raise
    except ValidationError:
        raise  # fix config — retry won't help

Raised errors stop the run; some callbacks record failures on the output instead. See Non-Fatal Errors.


Reaching the Step Limit

When the tool-calling loop reaches ExecutionConfig.max_steps (or max_iter when max_steps is unset), the agent does not hard-cut. On the final permitted step it injects a graceful wrap-up instruction, so the model returns a real summary of what it accomplished and what remains — not a placeholder.

Detect truncation with agent.last_stop_reason instead of string-matching:

python
from praisonaiagents import Agent, ExecutionConfig

agent = Agent(
    name="coder",
    instructions="You are a coding assistant.",
    execution=ExecutionConfig(max_steps=50),
)
result = agent.start("Refactor the auth module across all files")

if agent.last_stop_reason == "max_steps":
    result = agent.start("Continue from where you left off.")

On a gateway, surface the reason to chat users — see Turn Completion Notes.

On the CLI: when a truncated run reaches praisonai run / praisonai-code run, the wrapper reports it distinctly from success — exit code 2 (not 0), status: "truncated" under --output json / --output stream-json with the wrap-up summary preserved in result, plus a one-line stderr warning in interactive mode. See praisonai run → Exit Codes.

Step Budget

Cap tool-use steps and detect graceful truncation with last_stop_reason


Best Practices

Catch the most specific class you can handle

Use ToolExecutionError when you only care about tool failures; reserve PraisonAIError for top-level logging.

Log structured context

Include e.error_category, e.agent_id, and e.run_id in observability hooks — they correlate across multi-agent runs.

Don't swallow ValidationError

Validation failures usually mean a programming or config bug. Fix the root cause instead of retrying blindly.

Pair with loop guard for retry loops

Loop-guard HALT raises ToolExecutionError. Combine with Loop Guard when tools may repeat indefinitely.

Treat an LLMResponseError as a real failure, not an empty answer

A failed tool-calling loop now raises LLMResponseError instead of returning "". Catch it explicitly (from praisonaiagents.llm import LLMResponseError) so retries and observability see the actual error rather than a silent empty string.

Use PraisonAIError as the top-level catch for the whole wrapper

Every praisonai.* root exception (RecipeError, CLIExecutionError, JobQueueFull, ToolTimeoutError, and the rest listed under Wrapper exceptions) inherits from praisonaiagents.PraisonAIError. One top-level except PraisonAIError works as a safety net — reserve specific types for branches that handle the failure differently.


Related