/

Errors

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)").

The KarboneError hierarchy

TypeWhenKey fields
UnauthorizedHTTP 401, or a malformed on-premise JWT (see below)status, message, code
TemplateNotFoundHTTP 404 on a call addressing a known template idid, status, message
RenderNotFoundHTTP 404 on renders.download(id)id, status, message
PayloadTooLargeHTTP 413status, message
UnsupportedTemplateFormatHTTP 415status, message
BadRequestHTTP 400 or 422 — rejected metadata, invalid JSON, missing field, empty template…status, message
RenderFailedHTTP 500 on a render endpointstatus, message
UnexpectedAny other non-2xx status, or a 2xx response with success: falsestatus, message, body
InvalidRequestA request rejected client-side before any call (e.g. batch without startAsync)reason
TransportConnection, timeout or I/O failurecause
SerializationA response body that could not be parsedcause, body
TemplateReadLocal template content could not be readsource, 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.

Handling errors with Raise

The 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.

The blocking facade

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.

Malformed on-premise tokens

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.