Skip to content

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:

php
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:

php
} 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.

php
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:

php
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

php
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

php
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

php
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

php
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

php
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

php
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:

php
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:

php
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:

php
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:

php
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

Released under the MIT License.