13. Error Handling¶
Every failure is reported through the common zlink::framework error model. The form you receive it
in is one of two, depending on the consumption method.
result_t— for.result()or a callback submit. Branch withoperator bool, access detail witherror().- A
framework_exception_texception — forco_awaitandfetch<T>(). It haskind(),what(),is_retriable().
The same failure is just expressed in two forms — the classification is identical.
Error Kind Mapping¶
| kind | When | retriable |
|---|---|---|
request_protocol_error |
Invalid configuration/input: bad base_url/scheme, timeout 0 or below, 0-byte response body cap, empty header name, path not starting with /, multiple body sources, nullptr coroutine scheduler, https on a build with no OpenSSL |
✗ |
request_failed |
Transport failure (connection refused/dropped, TLS verification failure), 4xx/5xx on the typed path, redirect limit exceeded, response body cap exceeded, proxy CONNECT rejected | transport is ✓, the rest ✗ |
timeout |
Client/request timeout exceeded. On a coroutine client, the timeout is computed from the moment it's registered with the scheduler queue | ✓ |
payload_decode_failed |
Response JSON decode failure, corrupted gzip/deflate body | ✗ |
closed |
An uninitialized client (used after default-constructing client_t{}), a custom execute scheduler rejecting work registration |
✗ |
A configuration error (request_protocol_error) is deliberately distinguished from a transport
failure — because it's a code bug, so retrying is meaningless. It's often thrown right away, at
setter/build() time.
The result_t Pattern¶
auto result = client.get ("/players/7281").submit<player_profile_t> ().result ();
if (!result) {
const auto *error = result.error ();
switch (error->kind ()) {
case zlink::framework::framework_error_kind_t::timeout:
metrics.count ("player_lookup.timeout");
break;
case zlink::framework::framework_error_kind_t::payload_decode_failed:
log_error ("schema mismatch: {}", error->what ());
break;
default:
log_error ("player lookup failed: {}", error->what ());
}
return std::nullopt;
}
return result.value ().body;
The Exception Pattern (co_await / fetch)¶
try {
auto profile = client.get ("/players/7281").fetch<player_profile_t> ();
render (profile);
}
catch (const zlink::framework::framework_exception_t &error) {
if (error.is_retriable ()) {
schedule_retry ();
} else {
report_permanent_failure (error.what ());
}
}
Which Side Is 4xx/5xx On¶
submit<T>()/fetch<T>()(typed): a failure —request_failed, "HTTP request failed with status 404".submit_raw(): a success — you branch on the status directly (6. Handling Responses).
If the business logic cares about a status like 404/409, use the raw path; if "200 + DTO, otherwise a failure" is right, use the typed path.
The Relationship Between is_retriable And Automatic Retry¶
The scope retry(attempts) (Chapter 10) automatically retries
is exactly the failures where is_retriable() == true. Using the same criterion when writing your
own retry loop keeps it consistent.