Error Handling
Strategies for handling errors in Atlas-powered applications.
Exception Hierarchy
Atlas maps provider HTTP errors and configuration problems to typed exceptions:
AtlasException (base — extends RuntimeException, catch all Atlas errors)
├── ProviderException (base for any failed provider call; catch to handle them all)
│ ├── AuthenticationException (401 — invalid API key)
│ ├── AuthorizationException (403 — model access denied)
│ ├── RateLimitException (429 / 529 — rate limited or overloaded, with retry info)
│ ├── InvalidRequestException (400 — malformed/invalid request)
│ ├── ModelNotFoundException (404 — unknown model)
│ ├── ServerException (5xx — provider-side server error)
│ └── ConnectionException (network failure before any response; statusCode is null)
├── UnsupportedFeatureException (modality not supported by provider)
├── ProviderNotFoundException (provider key not registered)
├── AgentNotFoundException (agent key not found)
├── ToolNotFoundException (tool name not in registry)
└── MaxStepsExceededException (executor exceeded step limit)Catch ProviderException to handle any failed provider call, or a specific subclass for a category. Every ProviderException exposes ->provider, ->model, ->statusCode (null for ConnectionException), and ->providerMessage.
All exceptions live in Atlasphp\Atlas\Exceptions.
Provider Exceptions
Atlas automatically maps HTTP error codes to specific exception types:
use Atlasphp\Atlas\Exceptions\AuthenticationException;
use Atlasphp\Atlas\Exceptions\AuthorizationException;
use Atlasphp\Atlas\Exceptions\RateLimitException;
use Atlasphp\Atlas\Exceptions\ProviderException;
try {
$response = Atlas::text(Provider::OpenAI, 'gpt-4o-mini')
->message('Hello')
->asText();
} catch (AuthenticationException $e) {
// 401 — Invalid or missing API key
// $e->providerMessage carries the provider's real reason (e.g. "Incorrect API key provided")
} catch (AuthorizationException $e) {
// 403 — No access to this model; $e->providerMessage explains why
} catch (RateLimitException $e) {
// 429 — Too many requests
$retryAfter = $e->retryAfter; // Seconds to wait (from Retry-After header)
} catch (ProviderException $e) {
// All other HTTP errors (400, 500, etc.)
$e->statusCode; // HTTP status code
$e->providerMessage; // Real provider error message (every provider)
$e->provider; // Provider name (e.g., 'openai')
}$e->providerMessage carries the provider's actual error text across all providers — including authentication (401) and authorization (403) failures — regardless of how each one shapes its error response.
Inspecting the Raw Response
When providerMessage isn't enough, read the provider's raw error body straight off the exception — no need to dig through the previous exception:
} catch (ProviderException $e) {
$e->responseBody(); // decoded array, or null if no HTTP response (e.g. a connection failure)
$e->rawResponse(); // raw body string, or null
}Both return null for a ConnectionException, which has no HTTP response.
Provider interrogation calls (Atlas::provider(...)->models() and ->voices()) throw the same typed exceptions — e.g. an invalid key surfaces as AuthenticationException, not a raw HTTP-client error.
Connection Failures
Network-level failures that occur before the provider returns any response — connection timeouts, DNS failures, refused connections — surface as a ConnectionException. Unlike ProviderException, it carries no HTTP status because no response was received. These failures are retried automatically as transient errors (controlled by atlas.retry.errors) before the exception is thrown.
use Atlasphp\Atlas\Exceptions\ConnectionException;
try {
$response = Atlas::text(Provider::OpenAI, 'gpt-4o-mini')
->message('Hello')
->asText();
} catch (ConnectionException $e) {
// Network failure reaching the provider (timeout, DNS, refused)
$e->provider; // e.g. 'openai'
$e->model; // e.g. 'gpt-4o-mini'
}ConnectionException extends AtlasException, so a catch (AtlasException) block catches it too.
Streaming Errors
When a provider reports an error mid-stream (after the stream has started), Atlas detects the error event and throws a ProviderException while you iterate the stream — it does not silently truncate the response. Wrap stream consumption in a try/catch:
use Atlasphp\Atlas\Exceptions\ProviderException;
try {
foreach (Atlas::text(Provider::OpenAI, 'gpt-4o')->message('Hi')->asStream() as $chunk) {
echo $chunk->text;
}
} catch (ProviderException $e) {
// Provider errored partway through the stream
}Catching All Atlas Errors
use Atlasphp\Atlas\Exceptions\AtlasException;
try {
$response = Atlas::agent('assistant')
->message('Hello')
->asText();
} catch (AtlasException $e) {
// Catches any Atlas exception (provider errors, config errors, etc.)
Log::error('Atlas error', ['message' => $e->getMessage()]);
}Agent & Configuration Exceptions
Agent Not Found
use Atlasphp\Atlas\Exceptions\AgentNotFoundException;
try {
$response = Atlas::agent('unknown-agent')->message('Hi')->asText();
} catch (AgentNotFoundException $e) {
// "Agent [unknown-agent] is not registered."
}Max Steps Exceeded
use Atlasphp\Atlas\Exceptions\MaxStepsExceededException;
try {
$response = Atlas::agent('assistant')->message('Hi')->asText();
} catch (MaxStepsExceededException $e) {
// Agent executor exceeded the configured step limit
// $e->limit — the max steps value
// $e->steps — the steps completed before exceeding
}Unsupported Feature
use Atlasphp\Atlas\Exceptions\UnsupportedFeatureException;
try {
$response = Atlas::image(Provider::Anthropic, 'claude-sonnet-4-5-20250929')
->prompt('Draw a cat')
->asImage();
} catch (UnsupportedFeatureException $e) {
// Provider does not support this modality
}Tool Not Found
use Atlasphp\Atlas\Exceptions\ToolNotFoundException;
try {
$response = Atlas::agent('assistant')
->tools(['nonexistent_tool'])
->message('Hi')
->asText();
} catch (ToolNotFoundException $e) {
// "Tool [nonexistent_tool] is not registered."
}Provider Not Found
use Atlasphp\Atlas\Exceptions\ProviderNotFoundException;
try {
$response = Atlas::text('invalid-provider', 'some-model')
->message('Hello')
->asText();
} catch (ProviderNotFoundException $e) {
// Provider key not registered in config
}Tool Error Handling
In tool handlers, return error strings instead of throwing exceptions. This lets the AI retry or adjust its approach:
public function handle(array $args, array $context): mixed
{
try {
$order = Order::findOrFail($args['order_id']);
return $order->toArray();
} catch (ModelNotFoundException $e) {
return 'Order not found: '.$args['order_id'];
}
}When a tool returns an error string, the AI can try a different approach. If a tool throws an exception, Atlas catches it and sends the exception message back to the model as an error result — the agent loop continues. The AgentToolCallFailed event fires with the original exception type so listeners can respond appropriately (e.g., retry on RateLimitException).
WARNING
Exception messages are sent verbatim to the model. Ensure your tool exceptions do not contain sensitive information (credentials, file paths, etc.).
Rate Limit Handling
The RateLimitException includes retry information from the provider:
use Atlasphp\Atlas\Exceptions\RateLimitException;
try {
$response = Atlas::text(Provider::OpenAI, 'gpt-4o')
->message('Hello')
->asText();
} catch (RateLimitException $e) {
$seconds = $e->retryAfter;
if ($seconds) {
// Queue a retry after the specified delay
dispatch(fn () => $this->retry($input))
->delay(now()->addSeconds($seconds));
}
}Provider Fallback
Build resilience by falling back across providers:
class ResilientService
{
public function respond(string $input): TextResponse
{
$providers = [
[Provider::OpenAI, 'gpt-4o-mini'],
[Provider::Anthropic, 'claude-sonnet-4-5-20250929'],
];
foreach ($providers as [$provider, $model]) {
try {
return Atlas::text($provider, $model)
->message($input)
->asText();
} catch (RateLimitException $e) {
Log::warning("Rate limited on {$provider->value}, trying next");
continue;
} catch (ProviderException $e) {
Log::warning("Provider error on {$provider->value}: {$e->getMessage()}");
continue;
}
}
throw new \RuntimeException('All providers failed');
}
}Laravel Exception Handler
Integrate Atlas exceptions into your application's exception handler for consistent error responses:
use Atlasphp\Atlas\Exceptions\AtlasException;
use Atlasphp\Atlas\Exceptions\RateLimitException;
use Atlasphp\Atlas\Exceptions\AuthenticationException;
// In bootstrap/app.php or your exception handler
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (RateLimitException $e) {
return response()->json([
'error' => 'Service temporarily unavailable. Please retry.',
'retry_after' => $e->retryAfter,
], 429);
});
$exceptions->render(function (AuthenticationException $e) {
Log::critical('AI provider authentication failed', [
'message' => $e->getMessage(),
]);
return response()->json([
'error' => 'AI service configuration error.',
], 500);
});
$exceptions->render(function (AtlasException $e) {
return response()->json([
'error' => 'An error occurred processing your request.',
], 500);
});
})Next Steps
- Testing — Test error scenarios with Atlas::fake()
- Middleware — Add error handling middleware