Karbone never throws for expected failure modes. Every SDK method is declared as context(_: Raise<KarboneError>) suspend fun ..., using Arrow’s Raise to make failure part of the return type instead of an exception.
Every KarboneError.Api also exposes code, Carbone’s own error code when the response carries one. For example a test API key may only produce PDF output: asking for DOCX yields Unexpected(status=200, code="w117", message="With a test key, you can only convert in the following format (pdf)").
KarboneError hierarchy| Type | When | Key fields |
|---|---|---|
Unauthorized | HTTP 401, or a malformed on-premise JWT (see below) | status, message, code |
TemplateNotFound | HTTP 404 on a call addressing a known template id | id, status, message |
RenderNotFound | HTTP 404 on renders.download(id) | id, status, message |
PayloadTooLarge | HTTP 413 | status, message |
UnsupportedTemplateFormat | HTTP 415 | status, message |
BadRequest | HTTP 400 or 422 — rejected metadata, invalid JSON, missing field, empty template… | status, message |
RenderFailed | HTTP 500 on a render endpoint | status, message |
Unexpected | Any other non-2xx status, or a 2xx response with success: false | status, message, body |
InvalidRequest | A request rejected client-side before any call (e.g. batch without startAsync) | reason |
Transport | Connection, timeout or I/O failure | cause |
Serialization | A response body that could not be parsed | cause, body |
TemplateRead | Local template content could not be read | source, cause |
Every row above except the last four (InvalidRequest, Transport, Serialization, TemplateRead) implements KarboneError.Api, which exposes status and message uniformly. message is always the raw error text from the Carbone response, never rewritten or translated — safe to log or surface as-is.
Every KarboneError has a describe(): String for a human-readable summary, useful in logs. See the API reference for the full shape of each error type.
RaiseThe idiomatic way to call Karbone is inside a Raise<KarboneError> scope, most simply Arrow’s either:
import arrow.core.raise.either
val result = either { karbone.renders.render(template, data) } // Either<KarboneError, RenderedDocument>
result.fold(
{ error -> log.warn { "Render failed: ${error.describe()}" } },
{ document -> saveToDisk(document) },
)recover lets you fall back to a value instead of propagating:
import arrow.core.raise.recover
suspend fun statusOrUnknown(karbone: Karbone): ApiStatus = recover({ karbone.status() }) { error ->
ApiStatus(success = false, code = null, message = error.describe(), version = null)
}fold runs matching success/failure handlers directly, without materializing an Either:
import arrow.core.raise.fold
fold(
block = { karbone.templates.delete(id) },
recover = { error -> log.warn { error.describe() } },
transform = { log.info { "Deleted $id" } },
)Because KarboneError is sealed, you can also when on it exhaustively wherever that reads better than Either combinators.
Karbone.blocking() wraps every call with either { ... }.fold(...), throwing KarboneException on the Left side:
public class KarboneException(public val error: KarboneError) : RuntimeException(...)error is the same KarboneError value that Raise would have raised; getMessage() is error.describe(). This is the facade to reach for from Java, or from any Kotlin code that has not adopted Raise.
carbone-ee answers a malformed bearer token with HTTP 500 and an error message containing "Invalid JSON Web Token", rather than a 401. Karbone detects that message and maps it to KarboneError.Unauthorized regardless of the HTTP status, so callers get a consistent Unauthorized for every authentication failure — missing token, forged token, or expired token — whatever status code the server happens to answer with.