Controlled Failure Handling in Ballerina with check and error
Use Ballerina's error type and check keyword to make network failures explicit at compile time instead of handling them as runtime surprises.
13 Sept 2026, 12:03 UTC

A network blip becomes a service outage
You are integrating a Ballerina service with an external payment API. The call works in local testing, but in production the network blips once and the service returns a raw fault instead of a controlled fallback. The problem is rarely the network itself. It is that failure was treated as an afterthought instead of part of the function's type signature.
Ballerina treats an error as a value. A function that can fail declares a return type such as string|error, a union of the success type and the built-in error type. The compiler then requires every caller to address that error branch. This shifts failure handling from a runtime convention to a compile-time requirement.
Version assumption: this applies to current Ballerina releases in which the error type and check keyword are stable language features.
Propagate failures with check
The check keyword automates error propagation. If an expression returns T|error and the error branch occurs, check returns that error from the current function immediately. You do not need try-catch blocks for ordinary propagation.
Here is a simplified service that calls an external endpoint:
import ballerina/http;
import ballerina/log;
service /payments on new http:Listener(8080) {
resource function charge(string orderId, float amount) returns string|error {
http:Client paymentEp = check new("https://payments.example.internal");
string|error result = check paymentEp->/charge.post({orderId: orderId, amount: amount});
if result is string {
return result;
}
log:printError("payment failure for " + orderId, result);
return error("payment unavailable");
}
}
Run bal compile from the project root. It requires no special privileges. If you remove check from either call, compilation fails because the error branch is unhandled. Run bal run to start the listener, and make sure port 8080 is available or change the listener port.
A practical verification is to point paymentEp at an unreachable host and confirm the service returns payment unavailable instead of an unhandled fault. Do not assume this behavior until you observe it in your own environment.
Suppress intentionally with ignore
Sometimes an error code is safe to disregard. Ballerina provides ignore for that decision:
check paymentEp->/charge.post(...) ignore "ERR_1003";
This suppresses only the specified error code; other errors still propagate. Using ignore broadly hides real failures, so reserve it for codes you have reviewed and judged harmless.
Limitation: deep stacks obscure origins
Heavy use of check across deep call stacks can make the original failure harder to trace without structured logging. Because check returns immediately, the stack context can collapse by the time the error reaches a handler. Pair boundary calls with log:printError or equivalent context so operators can identify which external call failed.
Legacy Ballerina code that uses older throw/catch patterns may need migration before adopting check consistently. Mixing styles in the same service is possible but can obscure which errors are enforced by the type system.
Make failures explicit at the boundary
Start at your service's external boundaries: HTTP clients, database connectors, and message queues. Declare T|error returns, use check for propagation, and add logging where the error enters your system. Run bal compile after each change; the compiler will tell you where you forgot to handle a failure.
Ballerina's model does not remove network risk. It makes that risk visible before the code runs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.