Esse wraps Elasticsearch/OpenSearch client exceptions into a consistent hierarchy so your code does not need to branch on the ES vs OS SDK.
Hierarchy
Esse::Error├── Esse::Transport::ServerError # base for any HTTP server error│ ├── BadRequestError # 400│ ├── UnauthorizedError # 401│ ├── ForbiddenError # 403│ ├── NotFoundError # 404│ ├── RequestTimeoutError # 408│ ├── ConflictError # 409│ ├── RequestEntityTooLargeError # 413│ ├── UnprocessableEntityError # 422│ ├── InternalServerError # 500│ ├── BadGatewayError # 502│ ├── ServiceUnavailableError # 503│ └── GatewayTimeoutError # 504│├── Esse::Transport::ReadonlyClusterError # raised on writes when cluster.readonly == true└── Esse::Transport::BulkResponseError # bulk partial failureCatching errors
Most common patterns:
begin UsersIndex.importrescue Esse::Transport::ReadonlyClusterError # cluster is in readonly mode — skiprescue Esse::Transport::BulkResponseError => e e.response # full response hash e.items # failed items onlyrescue Esse::Transport::ServerError => e # other HTTP-level failurerescue Esse::Error => e # any Esse errorendNotFoundError nuance
NotFoundError is raised on document/index get, delete, etc. Some lifecycle operations silently ignore it (for example deleting an already-missing index), but UsersIndex.get(id: 1) will raise when the document is missing.
Opt out of raising with client-level ignore:
UsersIndex.get(id: 1, ignore: [404])# returns nil instead of raisingBulkResponseError
A bulk request can succeed at the HTTP level while containing per-item failures. Esse inspects the response and raises BulkResponseError when any item has an error:
begin UsersIndex.importrescue Esse::Transport::BulkResponseError => e e.items.each do |item| op, data = item.first puts "#{op} failed for id=#{data[:_id]}: #{data.dig(:error, :reason)}" endendReadonlyClusterError
Raised preemptively when:
cluster.readonly = true, and- you call a write operation (
bulk,index,update,delete,create_index,delete_index,reset_index, etc.).
Readonly clusters still serve reads (search, count, get, mget).
Writing your own rescue
def safe_index(record) UsersIndex.index(id: record.id, body: record.as_indexed)rescue Esse::Transport::ReadonlyClusterError Rails.logger.warn("Skipping index — cluster is readonly")rescue Esse::Transport::RequestEntityTooLargeError # split and retry — or let Esse's bulk retry handle itendCoercing client exceptions
If you write a custom transport plugin or call the ES client directly, wrap calls in:
transport.coerce_exception { client.some_call }This converts ES/OS SDK exceptions into the matching Esse::Transport::* subclass.