Choosing Ballerina Error Propagation for Low‑Latency Observable Microservices
Decide whether to use Ballerina’s checked error with the ? operator, explicit error checks, or panic when calling external APIs in a microservice, based on latency, observability, and safety constraints.
11 Apr 2026, 14:55 UTC

Decision: Error Propagation Style for Ballerina Microservice
When a Ballerina service calls external HTTP APIs, the team must decide how errors from those calls are propagated to the caller while meeting low latency, observable failures, no silent drops, and compatibility with isolated functions and workers.
Options Comparison
| Option | Syntax | Boilerplate | Observability | Retry / Classification |
|---|---|---|---|---|
A – Checked error with ? operator |
returns T|error; use client->/path.get()? |
Minimal – only the return type declaration | Relies on caller to log/wrap; context can be lost unless wrapped | Requires explicit wrapping if retry logic is needed |
B – Explicit if err != nil checks |
Call returns error; inspect and create custom error | Higher – each call needs a check and possible wrap | Allows per‑dependency logging, metrics, and error enrichment | Natural place to inject retry, classification, or circuit‑breaker |
C – panic for unrecoverable errors |
Invoke panic when an invariant is violated |
None – terminates the worker | No observability for expected failures; drops in‑flight requests | Not applicable for retry |
Trade‑offs
The ? operator keeps code concise and lets the compiler enforce handling at the call site, but it pushes responsibility upward and can strip away useful context unless the error is wrapped before returning.
Explicit checks increase verbosity but give a dedicated spot to add logs, metrics, and custom error types, which satisfies the observability and low‑latency retry requirements.
panic should be reserved for programming errors (e.g., nil pointer dereference) because it stops the worker and prevents any further request processing, violating the "no silent drops" constraint.
Concrete Implementation
import ballerina/http;
import ballerina/log;
// Service boundary function – isolated, returns a checked error
isolated function getUser(string userId) returns User|error {
// HTTP client call; the ? operator propagates any error upward
User|error resp = check httpClient->/users/{userId}.get();
return resp;
}
// Caller that decides to wrap the error for observability
service /api on new http:Listener(9090) {
resource function get getUser(string userId) returns User|error {
User|error res = getUser(userId);
if res is error {
// Enrich with context before returning to the caller
log:printError("Failed to fetch user", "error", res);
return error("service unavailable: unable to retrieve user");
}
return res;
}
}
Verification Steps
- Create a minimal Ballerina project with the above service and a mock HTTP client that returns an error.
- Run
bal build– the compiler should reject any call site that does not handle the returnederrortype. - Run
bal testwith a unit test that mocks the HTTP client to return a specific error; assert that the service returns the wrapped error and that a log statement is emitted. - Inspect the generated OpenTelemetry spans (if using the Ballerina observability package) to confirm that the error event appears at the top‑level handler and is not swallowed.
These steps confirm that the chosen pattern compiles under Ballerina 2201.x, respects the isolated‑function constraint, and provides a place for observability enrichment.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.