> For the complete documentation index, see [llms.txt](https://azutopia.gitbook.io/azu/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://azutopia.gitbook.io/azu/error-handling/handle-errors-gracefully.md).

# Handle Errors Gracefully

This guide shows you how to implement robust error handling in your Azu application.

## Built-in Error Handling

Azu provides a Rescuer handler for catching exceptions:

```crystal
MyApp.start [
  Azu::Handler::Rescuer.new,
  # ... other handlers
]
```

## HTTP Error Responses

Use built-in error responses:

```crystal
def call
  user = User.find?(params["id"])

  unless user
    raise Azu::Response::NotFound.new("/users/#{params["id"]}")
  end

  UserResponse.new(user)
end
```

Available error responses:

* `Azu::Response::BadRequest` (400)
* `Azu::Response::Unauthorized` (401)
* `Azu::Response::Forbidden` (403)
* `Azu::Response::NotFound` (404)
* `Azu::Response::ValidationError` (422)
* `Azu::Response::InternalServerError` (500)

## Custom Error Handler

Create a comprehensive error handler:

```crystal
class ErrorHandler < Azu::Handler::Base
  Log = ::Log.for(self)

  def call(context)
    call_next(context)
  rescue ex : Azu::Response::Error
    handle_known_error(context, ex)
  rescue ex : JSON::ParseException
    handle_json_error(context, ex)
  rescue ex : CQL::RecordNotFound
    handle_not_found(context, ex)
  rescue ex : CQL::RecordInvalid
    handle_validation_error(context, ex)
  rescue ex
    handle_unknown_error(context, ex)
  end

  private def handle_known_error(context, ex : Azu::Response::Error)
    respond_with_error(context, ex.status, ex.message)
  end

  private def handle_json_error(context, ex)
    respond_with_error(context, 400, "Invalid JSON: #{ex.message}")
  end

  private def handle_not_found(context, ex)
    respond_with_error(context, 404, "Resource not found")
  end

  private def handle_validation_error(context, ex)
    respond_with_error(context, 422, "Validation failed", ex.errors)
  end

  private def handle_unknown_error(context, ex)
    Log.error(exception: ex) { "Unhandled error" }

    if ENV["AZU_ENV"] == "production"
      respond_with_error(context, 500, "Internal server error")
    else
      respond_with_error(context, 500, ex.message, ex.backtrace)
    end
  end

  private def respond_with_error(context, status, message, details = nil)
    context.response.status_code = status
    context.response.content_type = "application/json"

    body = {error: message}
    body = body.merge({details: details}) if details

    context.response.print(body.to_json)
  end
end
```

## Endpoint-Level Error Handling

Handle errors within endpoints:

```crystal
struct CreateOrderEndpoint
  include Azu::Endpoint(CreateOrderRequest, OrderResponse)

  post "/orders"

  def call : Azu::Response
    validate_inventory
    order = create_order

    status 201
    OrderResponse.new(order)
  rescue ex : InsufficientInventoryError
    status 422
    ErrorResponse.new("Insufficient inventory: #{ex.message}")
  rescue ex : PaymentDeclinedError
    status 402
    ErrorResponse.new("Payment declined: #{ex.message}")
  end

  private def validate_inventory
    # Check inventory...
  end

  private def create_order
    # Create order...
  end
end
```

## Error Response Format

Create a consistent error response:

```crystal
struct ErrorResponse
  include Azu::Response

  def initialize(
    @message : String,
    @code : String? = nil,
    @details : Hash(String, String)? = nil
  )
  end

  def render
    response = {
      error: {
        message: @message
      }
    }

    response[:error][:code] = @code if @code
    response[:error][:details] = @details if @details

    response.to_json
  end
end
```

## Logging Errors

Log errors with context:

```crystal
class ErrorLogger < Azu::Handler::Base
  Log = ::Log.for(self)

  def call(context)
    call_next(context)
  rescue ex
    log_error(context, ex)
    raise ex
  end

  private def log_error(context, ex)
    Log.error(exception: ex) { {
      error_class: ex.class.name,
      message: ex.message,
      path: context.request.path,
      method: context.request.method,
      request_id: context.request.headers["X-Request-ID"]?,
      user_agent: context.request.headers["User-Agent"]?
    }.to_json }
  end
end
```

## Error Monitoring

Send errors to external monitoring:

```crystal
class ErrorReporter < Azu::Handler::Base
  def call(context)
    call_next(context)
  rescue ex
    report_error(context, ex)
    raise ex
  end

  private def report_error(context, ex)
    return if ENV["AZU_ENV"] != "production"

    # Send to Sentry, Honeybadger, etc.
    Sentry.capture_exception(ex, extra: {
      path: context.request.path,
      method: context.request.method
    })
  end
end
```

## Retry Logic

Implement retry for transient errors:

```crystal
def with_retry(max_attempts = 3, &)
  attempts = 0

  loop do
    attempts += 1
    return yield
  rescue ex : Timeout::Error | IO::Error
    raise ex if attempts >= max_attempts

    Log.warn { "Attempt #{attempts} failed, retrying..." }
    sleep (2 ** attempts).seconds
  end
end

# Usage
def call
  with_retry do
    external_api.fetch_data
  end
end
```

## Circuit Breaker

Prevent cascading failures:

```crystal
class CircuitBreaker
  enum State
    Closed
    Open
    HalfOpen
  end

  def initialize(
    @failure_threshold = 5,
    @reset_timeout = 30.seconds
  )
    @state = State::Closed
    @failures = 0
    @last_failure_time = Time.utc
  end

  def call(&)
    case @state
    when .open?
      if Time.utc - @last_failure_time > @reset_timeout
        @state = State::HalfOpen
      else
        raise CircuitOpenError.new
      end
    end

    begin
      result = yield
      on_success
      result
    rescue ex
      on_failure
      raise ex
    end
  end

  private def on_success
    @failures = 0
    @state = State::Closed
  end

  private def on_failure
    @failures += 1
    @last_failure_time = Time.utc

    if @failures >= @failure_threshold
      @state = State::Open
    end
  end
end
```

## Graceful Degradation

Provide fallback behavior:

```crystal
def call
  data = fetch_from_primary
rescue ex : ServiceUnavailableError
  Log.warn { "Primary service unavailable, using cache" }
  data = fetch_from_cache

  if data.nil?
    Log.error { "No cached data available" }
    raise ex
  end

  data
end
```

## See Also

* [Create Custom Errors](/azu/error-handling/create-custom-errors.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://azutopia.gitbook.io/azu/error-handling/handle-errors-gracefully.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
