Skip to the documentation
Ruby

Errors

Every failure raises. One class for a refusal, one for no answer, and a request id on every API error.

Catching one

rescue_errors.rb
message = {from: "[email protected]", to: "[email protected]", subject: "Your September invoice", text: "Invoice attached."} begin  client.emails.send(message)rescue OpenEmail::ApiError => error  warn "#{error.code} #{error.param} #{error.message}" if error.validation?  warn client.addresses.list_all.addresses.inspect if error.permission?  warn "try again in #{error.retry_after_seconds} seconds" if error.rate_limited?   warn "#{error.status} #{error.request_id}"  raiserescue OpenEmail::NetworkError => error  warn "no answer in time" if error.timeout?  raiseend

A permission_error on a send is usually the KEY’s send scope, a domain or an address it was not given, rather than the workspace, which is why the sample prints what addresses.list_all says this key may send as.

Each kind of refusal has a subclass of its own, so a rescue can pick the ones it handles by class and let the rest go on up.

rescue_by_class.rb
message = {from: "[email protected]", to: "[email protected]", subject: "Your September invoice", text: "Invoice attached."} begin  client.emails.send(message)rescue OpenEmail::ValidationError => error  warn "#{error.param}: #{error.message}"rescue OpenEmail::AuthenticationError, OpenEmail::PermissionError => error  warn "the key cannot do this: #{error.code}"  raiserescue OpenEmail::Error => error  warn "#{error.class}: #{error.message}"  raiseend

The classes

ClassWhen
OpenEmail::ErrorThe base of every error the gem defines, so rescue OpenEmail::Error catches all of them. It does not catch ArgumentError.
OpenEmail::ApiErrorThe API answered, and not with a success. Carries status, type, code, param, doc_url, request_id, retry_after_seconds, fields and body. Raised as itself when type is api_error, as a server fault is, and as the subclass for its type otherwise.
OpenEmail::InvalidRequestError, AuthenticationError, PermissionError, NotFoundError, ConflictError, ValidationError and RateLimitErrorSubclasses of ApiError, one for each type: invalid_request_error, authentication_error, permission_error, not_found_error, conflict_error, validation_error and rate_limit_error.
OpenEmail::NetworkErrorNo response arrived: DNS, TLS, a refused or dropped connection, or the timeout. Carries original, the exception underneath, which is also its cause, and timeout? is true when the timeout was the reason.
OpenEmail::WebhookSignatureErrorOpenEmail.verify_webhook_signature refused a delivery.
ArgumentErrorRaised before anything is sent: a missing or malformed key, an unusable base_url:, an empty id. It is the plain Ruby class, not an OpenEmail::Error, because it means the call itself is wrong.

What an ApiError carries

messageString
The API’s own sentence, written for a person and naming the offending value where there is one. Not a stable identifier, so switch on `code`.
statusInteger
The HTTP status of the answer.
typeString
One of the eight values in `OpenEmail::ERROR_TYPES`, a set that is frozen and will not grow. When the body names none, it is inferred from the status.
codeString
The specific failure, such as `from_address_forbidden` or `invalid_email_address`. Open and additive, so treat one you do not recognise as its `type`. It is `unrecognised_response` when the body was not the API’s error envelope.
paramString or nil
The field that was refused, as a dotted path such as `to.0`, when the failure names one.
doc_urlString or nil
A page about this failure, when the API names one.
request_idString or nil
The id the server logged the request under, from the body or the `x-request-id` header.
retry_after_secondsInteger, Float or nil
The wait the server asked for in `Retry-After`, in seconds, whether it sent a number or a date. nil when it sent none.
fieldsArray<Hash> or nil
One Hash per problem, each with `key` and `error`, such as `{key: "email", error: "email"}` when `forms.subscribe` refused the answers with a 422 `invalid_form_submission`. nil when the error lists none.
bodyHash or nil
The whole error response, parsed, with Symbol keys. nil when it was empty or not JSON.
PredicateTrue when
auth?type is authentication_error, a 401: no key, the wrong kind of credential, or a key we did not issue.
permission?permission_error, a 403: a real key without the scope or the From address it needs.
scope_missing?code is insufficient_scope, the 403 that names a missing scope.
invalid_request?invalid_request_error, a 400: a request that could not be understood. A message over the size ceiling comes back as a 422 message_too_large, so validation? is the predicate that catches it.
validation?validation_error, a 422: the schema refused it, and param names the field.
not_found?not_found_error, a 404: no such resource.
conflict?conflict_error, a 409: the resource is past the point where this could be done to it.
rate_limited?rate_limit_error, a 429. retry_after_seconds holds the wait when the server named one.
server_error?status is 500 or above. Quote request_id if you contact support.
retryable?status is 408, 429, 500, 502, 503 or 504.
step_up_required?code is step_up_required, the 403 an OAuth access token gets before a sensitive change until the person has verified a code.

Most predicates read type, the frozen half of the envelope, and each subclass stands for one type. code stays a String, because the API guarantees it is open and additive, so treat one you do not recognise as its type. A closed list would make a gem upgrade the price of reading a new failure mode.

A body that is not the API’s error envelope still raises an OpenEmail::ApiError, with type inferred from the status and code set to unrecognised_response. A success whose body is not JSON raises one too.

retryable? describes the status, not your call. A call that is safe to repeat has already been retried by the time it raises, and a 429 such as send_quota_exceeded or ai_quota_exceeded fails the same way until its allowance resets, so show it to a person rather than looping on it.

An exception your adapter raises becomes an OpenEmail::NetworkError once any retries the call allows are spent, with the original on original and cause. NameError, TypeError and ArgumentError are the exceptions: they mean a bug in the adapter, so they are raised unchanged and never retried.

request_id

Every OpenEmail::ApiError carries the request id the server sent, from the error body or the x-request-id header, and it is the only thing tying your failure to a line in the server’s log. A success returns the parsed body alone, so there is no request id to read on one.