Errors and Error Handling in Ballerina — Part I
This post is based on Ballerina Swan Lake Update 5
Ballerina does not have exceptions. It has a separate error basic type which is used to indicate errors in a program.
With the separate error type, errors in Ballerina are explicit and pretty much impossible to ignore by design. Any and all error values created belong to a builtin type named error
.
Below we’ve written a function that takes a string
username
and validates it. If the username
is of a length less than 6 or if it has spaces we want to consider it as an invalid username, else, the username
needs to be considered valid.
We use the builtin error
type to indicate an invalid username. For valid usernames, we can just return nil, which in Ballerina represents the absence of any other value. Therefore, we specify error?
which is the union of error
and nil as the return type. This is the same as error|()
.
function validateUsername(string username) returns error? {
if username.length() < 6 || username.indexOf(" ") !is () {
return error("invalid username");
}
}
If the username meets the criteria for an invalid username, we create and return an error. If not, nil is returned (implicitly here by falling off the end of the function).
Now wherever this function is called, we can check if the username is valid by checking whether the returned value is an error value or nil.
Error values
An error value has three components:
- message — a
string
representing the error message - cause — an optional component, which if available is also an error value that can be used to indicate the error that caused the current error
- detail — an immutable mapping value giving additional details about the error — empty mapping if no additional details are available. The detail mapping is of a type that is a subtype of
map<value:Cloneable>
.
Constructing error values and accessing the error message, cause, and detail mapping
An error value can be constructed using the error constructor expression.
error("InvalidLength",
reason = string `invalid length: ${username.length()}`)
Let’s look into the error constructor in detail based on some examples.
error invalidLengthErr = error("InvalidLength",
length = username.length(),
code = "E1001");
error invalidUsernameErr = error("InvalidUsername",
invalidLengthErr,
code = "E1011");
Here we construct two error values and assign them to variables named invalidLengthErr
and invalidUsernameErr
.
The first error value is used when constructing the second error. The first argument to an error constructor expression is expected to be a positional argument of type string
, which will be the error message. In the examples, they are “InvalidLength” and “InvalidUsername”.
A second positional argument if present is expected to be of a type that is a subtype of type error
(i.e., an error value), which will be the cause. In the first example, no cause is specified, whereas in the second example invalidLengthErr
is specified as the cause.
The rest of the arguments, if present, are expected to be named arguments representing the fields of the error detail mapping. In both the examples we have detail fields: length
and code
for invalidLengthErr
and just code
for invalidUsernameErr
.
Given that only the error message is mandatory, we can have an error constructor with just the error message.
error err = error("invalid username");
Since no detail arguments are specified, the detail would be an empty mapping for err
.
The message, cause, and detail mapping of an error value can be accessed using the message
, cause
, and detail
functions defined in the error lang library.
import ballerina/io;
import ballerina/lang.value;
public function main() {
// Invalid username for demonstration.
string username = "jo";
// Construct an error with no cause but with two detail fields.
error e1 = error("InvalidLength", length = username.length(),
code = "E1001");
// Access and print the message of `e1`.
string message = e1.message();
io:println(message); // InvalidLength
// Access the cause of `e1`.
error? cause = e1.cause();
// Since a cause wasn't specified, `cause` will be nil.
io:println(cause is ()); // true
// Access the detail mapping of `e1`.
map<value:Cloneable> & readonly detail = e1.detail();
// Print the length of the detail mapping of `e1`.
io:println(detail.length()); // 2
// Access the `code` field in the detail mapping and print it.
// Since from the static type of the detail mapping `
// map<value:Cloneable> & readonly`, the compiler does not
// know if the detail mapping has a field named `code`,
// member access needs to be used.
// The static type of the access is `value:Cloneable & readonly`
// which also includes nil for if the field is not present.
value:Cloneable & readonly code = detail["code"];
io:println(code); // E1001
// Construct an error with a cause but with no detail fields.
error e2 = error("InvalidUsername", e1);
io:println(e2.message()); // InvalidUsername
cause = e2.cause();
// Since `e1` was specified as the cause, `cause` will be `e1``.
io:println(cause); // error("InvalidLength",length=2,code="E1001")
io:println(cause === e1); // true
// Print the length of the detail mapping of `e2`.
// The length will be `0` since no arguments were specified
// for the detail mapping.
io:println(e2.detail().length()); // 0
}
An error value can be used similar to other values — e.g., can be passed as arguments, can be returned from functions, etc.
However, the error type (and an error value) does have some differences that differentiate it from the other types/values:
- An error value cannot be assigned to a variable of type
any
(or used where a value belonging toany
is expected) — theany
type in Ballerina represents all values in Ballerina, except for an error value. - An error value cannot be ignored using the wildcard (
_
).
The following results in compilation errors:
public function main() {
error e = error("error reason", code = 404);
any a = e; // compilation error
_ = e; // compilation error
}
Note that it is an error value that does not belong to any
, and therefore is distinguished from other values. But a structure of errors will belong to any
.
public function main() {
error[] arr = [error("error reason", code = 404)];
any a = arr; // valid
_ = arr; // valid
}
These together with the fact that values cannot be ignored implicitly in Ballerina help ensure that errors cannot be ignored.
An action or an expression can evaluate to an error value or can cause abrupt termination via panic.
Expressions evaluating to an error value
Various kinds of expressions can evaluate to an error value. For example, calling a function can evaluate to an error value if the function may return an error value. Lax access on JSON can also evaluate to an error. The static type of these expressions would therefore include error.
function fn(json j) returns error? {
json|error name = j.id;
error? validationRes = validateUsername(name);
}
function validateUsername(json|error username) returns error? {
}
User code reports errors via functions that return errors.
Functions returning error values
A function that may return an error value will have a compatible error type or types in the return type of the function.
import ballerina/io;
const INVALID_USERNAME = "InvalidUsername";
// A username validation function that returns an `error` value
// for invalid usernames, or `()` if the username is valid.
function validateUsername(string username) returns error? {
if username.length() < 6 {
return error(INVALID_USERNAME, reason = "invalid length");
}
if username.indexOf(" ") !is () {
return error(INVALID_USERNAME, reason = "contains spaces");
}
// `()` is returned if this point is reached, indicating a valid username.
}
public function main() {
error? res = validateUsername("ma");
io:println(res is error); // true
io:println(res); // error("InvalidUsername",reason="invalid length")
io:println(validateUsername("m a r y a m")); // error("InvalidUsername",reason="contains spaces")
io:println(validateUsername("maryam") is error); // false
}
The error needs to be handled wherever the function is called. For example, the following will result in compilation errors since the error scenario isn’t handled.
public function main() {
_ = validateUsername("ma"); // error: can't use `_` with potential error values
validateUsername("maryam"); // error: can't ignore the return value
}
Abrupt termination via panic
An error value can also result in abrupt termination via panic, if used in a panic
statement (or with checkpanic
explained later on).
If not explicitly handled with trap
(explained later on) along the call-stack, a panic results in program termination.
import ballerina/io;
const INVALID_ACC_ID = "InvalidAccountId";
const INVALID_ACC_ID_CODE = "E1011";
function updateAccount(int accountId, decimal amount) {
if accountId < 1000 {
panic error(INVALID_ACC_ID, code = INVALID_ACC_ID_CODE);
}
// Account update logic.
io:println("Updated Account for ID ", accountId, " , Amount: ", amount);
}
public function main() {
updateAccount(2500, 1500.00);
updateAccount(25, 1000.00);
}
While the first update will be successful, the second update attempt will panic. Since the panic is not trapped, the program will terminate.
$ bal run sample.bal
Updated Account for ID 2500 , Amount: 1500.00
error: InvalidAccountId {"code":"E1011"}
at sample:updateAccount(sample.bal:8)
sample:main(sample.bal:17)
The stack trace of the error associated with the panic is printed.
Panicking is only expected to be done in exceptional scenarios — i.e., non-recoverable scenarios (e.g., invalid state errors, index out of range errors, etc.). Business logic related errors would usually be returned.
Handling errors
If a function could potentially return an error value, the result of the function call could be used in an is
check to identify if an error occurred, and act accordingly. Note how in the following example the register
function only adds an account if the username validation does not return an error
(i.e., the username is valid).
import ballerina/log;
const INVALID_USERNAME = "InvalidUsername";
// A username validation function that returns an `error` value
// for invalid usernames, or `()` if the username is valid.
function validateUsername(string username) returns error? {
if username.length() < 6 {
return error(INVALID_USERNAME, reason = "invalid length");
}
if username.indexOf(" ") !is () {
return error(INVALID_USERNAME, reason = "contains spaces");
}
// `()` is returned if this point is reached, indicating a valid username.
}
function register(string username) {
error? validationRes = validateUsername(username);
if validationRes is error {
// `validationRes` is of type `error` within this block
log:printError("Error registering user, invalid username", validationRes);
} else {
// `validationRes` is of type nil within this block.
// Add registration logic.
}
}
By using the is
check against error
in the if condition we narrow the type of validationRes
to error
within the if block, which allows us to pass it directly as the second argument to log:printError
which expects an error
value.
check
In certain scenarios, if an expression (or action) evaluates to an error value, you may want to return the error value as is without further processing.
function register(string username) returns error? {
error? validationRes = validateUsername(username);
if validationRes is error {
// `validationRes` is of type `error` within this block
return validationRes;
} else {
// `validationRes` is of type nil within this block.
// Add registration logic.
}
}
Instead of using an is
check and returning as shown in the example, Ballerina provides a special kind of expression — the check
expression — as shorthand for the same. The register function can be simplified using check
as follows.
function register(string username) returns error? {
check validateUsername(username);
// We get here only if the `validateUsername` function returned nil.
// Add registration logic.
}
Let’s look at another example where the type of the expression with which check
is used is a union of error and a non-nil type.
import ballerina/io;
function get(map<decimal> configMap, string key) returns decimal|error {
if configMap.hasKey(key) {
return configMap.get(key);
}
return error("key not found");
}
function compare(map<decimal> configMap, string key, decimal threshold) returns boolean|error {
decimal|error config = get(configMap, key);
if config is error {
// The type of `config` is `error` here.
return config;
}
// Since the error case is handled in the if block and the error is always returned,
// the type of `config` is safely narrowed to the non-error type (`decimal`) here.
return config >= threshold;
}
public function main() {
map<decimal> configMap = {
factor: 5,
'default: 2
};
io:println(compare(configMap, "factor", 6)); // false
io:println(compare(configMap, "ratio", 0.6)); // error("key not found")
}
The compare
function can be simplified as follows using a check expression.
function compare(map<decimal> configMap, string key, decimal threshold)
returns boolean|error {
decimal config = check get(configMap, key);
return config >= threshold;
}
This can be simplified even further by making it an expression-bodied function.
function compare(map<decimal> configMap, string key, decimal threshold)
returns boolean|error =>
check get(configMap, key) >= threshold;
checkpanic
Similarly, you may want to terminate the program, if a particular expression (or action) evaluates to an error value. One way to do this would be to evaluate the expression to get the result, do the is
check, and panic if the result is an error.
For example, with the get
function that returns decimal|error
,
function compare(map<decimal> configMap, string key, decimal threshold)
returns boolean|error {
decimal|error config = get(configMap, key);
if config is error {
panic config;
}
// Since the error case is handled in the if block and always results
// in a panic, the type of `config` is safely narrowed to the
// non-error type (`decimal`) here.
return config >= threshold;
}
Alternatively, you could use the checkpanic
expression, which works similar to check
, except that instead of returning the error if the expression (or action) evaluates to error, it panics.
import ballerina/io;
function get(map<decimal> configMap, string key) returns decimal|error {
if configMap.hasKey(key) {
return configMap.get(key);
}
return error("key not found");
}
function compare(map<decimal> configMap, string key, decimal threshold)
returns boolean =>
checkpanic get(configMap, key) >= threshold;
public function main() {
map<decimal> configMap = {
factor: 5,
'default: 2
};
io:println(compare(configMap, "factor", 6));
io:println(compare(configMap, "ratio", 0.6));
io:println(compare(configMap, "default", 4));
}
Note how the return type of compare
can now be just boolean
rather than boolean|error
since the function does not return an error
, but panics on error instead.
$ bal run sample.bal
false
error: key not found
at sample:get(sample.bal:8)
sample:compare(sample.bal:12)
sample:main(sample.bal:22)
Note how the program terminates due to the panic. The last comparison does not run.
on fail
The semantic of check
is that it fails when the expression it is used with evaluates to an error. It is up to the enclosing block to decide how the failure needs to be handled.
With the examples we saw previously, the enclosing block would be the function, which implies that the error value simply needs to be returned from the function.
Alternatively, the check
expression could be enclosed within a block to which an on fail
clause is attached. In such a scenario, control is transferred to the on fail
clause with the associated error value. Similar to the previous examples, if the expression with which check
was used evaluates to an error value, the subsequent logic within the original block isn’t executed.
import ballerina/io;
import ballerina/log;
function get(map<decimal> configMap, string key) returns decimal|error {
if configMap.hasKey(key) {
return configMap.get(key);
}
return error("key not found");
}
function compare(map<decimal> configMap, string key, decimal threshold) {
do {
if key.trim().length() == 0 {
check error("cannot use a blank string as the key");
}
decimal config = check get(configMap, key);
io:println("Comparison: ", config >= threshold);
} on fail error e {
log:printError("config retrieval failed", e);
}
}
public function main() {
map<decimal> configMap = {
factor: 5,
'default: 2
};
compare(configMap, "factor", 4);
compare(configMap, "", 1);
compare(configMap, "ratio", 4);
}
The compare
function now has a do
statement with an on fail
clause attached to it. Within the do
block, check
is used with two expressions that can evaluate to an error value — if the key specified is a blank string or if the get
function returns an error value due to the key not being available. If none of these happen, in the last line of the do
block the comparison is done and printed using the io:println
function. Unlike the previous functions, now, if the expressions with which check
is used evaluate to error values, control is transferred to the on fail
clause rather than the error values being returned from the function. Therefore, the return type does not include an error type.
$ bal run sample.bal
Comparison: true
time = 2023-04-24T12:27:31.989+05:30 level = ERROR module = "" message = "config retrieval failed" error = "cannot use a blank string as the key"
time = 2023-04-24T12:27:32.031+05:30 level = ERROR module = "" message = "config retrieval failed" error = "key not found"
The on fail
clause can have any logic, which means it is also possible to handle the error as required and then return the error value from the function itself.
function compare(map<decimal> configMap, string key, decimal threshold)
returns boolean|error {
do {
if key.trim().length() == 0 {
check error("cannot use a blank string as the key");
}
decimal config = check get(configMap, key);
return config >= threshold;
} on fail error e {
log:printError("config retrieval failed", e);
return e;
}
}
on fail
provides a way to handle multiple errors in a single place.
However, note that on fail
is not similar to say a try-catch block in Java. Control is transferred to the on fail
clause only for usages of check
and fail
(see the next section). Returned errors or panics will not cause transfer of control to the on fail
clause.
import ballerina/io;
import ballerina/log;
function get(map<decimal> configMap, string key) returns decimal|error {
if configMap.hasKey(key) {
return configMap.get(key);
}
return error("key not found");
}
function compare(map<decimal> configMap, string key, string threshold)
returns boolean|error? {
do {
if configMap.length() == 0 {
return error("config map is empty");
}
if key.trim().length() == 0 {
panic error("cannot use a blank string as the key");
}
return check get(configMap, key) >= check decimal:fromString(threshold);
} on fail error e {
log:printError("config retrieval failed", e);
return e;
}
}
public function main() {
map<decimal> configMap = {
factor: 5,
'default: 2
};
// 1. Empty config map - error is returned, therefore, no transfer of control
// to the `on fail` clause.
boolean|error? v1 = compare({}, "factor", "4");
io:println(v1);
// 2. Unavailable key - used with `check`, therefore, control is transferred
// to the `on fail` clause, causing the error to be logged.
boolean|error? v2 = compare(configMap, "ratio", "4");
io:println(v2);
// 3. Blank string as key - panics, therefore, no transfer of control
// to the `on fail` clause. Also results in program termination.
boolean|error? v3 = compare(configMap, "", "1");
}
$ bal run sample.bal
error("config map is empty")
time = 2023-04-24T12:28:28.809+05:30 level = ERROR module = "" message = "config retrieval failed" error = "key not found"
error("key not found")
error: cannot use a blank string as the key
at sample:compare(sample.bal:20)
sample:main(sample.bal:48)
Note how the log message is logged only for the expression (that evaluated to an error value) that was used with check
— key not found.
The typed binding pattern in an on fail
clause is optional, meaning you could skip it if you don’t want to access the error value within the on fail
clause.
function compare(map<decimal> configMap, string key, decimal threshold)
returns boolean? {
do {
if key.trim().length() == 0 {
check error("cannot use a blank string as the key");
}
decimal config = check get(configMap, key);
return config >= threshold;
} on fail {
log:printError("config retrieval failed");
return ();
}
}
fail
Ballerina also supports the fail
statement, which works similar to a check
statement or expression, except that it can be used only with an expression that will always evaluate to an error value (i.e., the static type of the expression should be a subtype of error
).
So instead of using check
with an error constructor (or any expression that is of a static type that is a subtype of error
), we can use fail
.
function compare(map<decimal> configMap, string key, decimal threshold)
returns boolean|error {
do {
if key.trim().length() == 0 {
fail error("cannot use a blank string as the key");
}
decimal config = check get(configMap, key);
return config >= threshold;
} on fail error e {
log:printError("config retrieval failed", e);
return e;
}
}
Previously (first example in the section on on fail
), we used check
with an error constructor if the key was a blank string. Now we have used a fail
statement instead.
Handling panics
Given that a particular expression (or action) could panic, you may also want to avoid program termination by handling the associated error value explicitly. This could be done using the trap
expression.
Let’s use the get
function from the lang.map
lang library as an example. When used with a map of type map<int|string>
, the return type of the function is int|string
. This function panics if the key specified is not available in the map.
import ballerina/io;
public function main() {
map<int|string> m = {
factor: 5,
'default: 2,
name: "def"
};
int|string ratio = m.get("ratio"); // panics
io:println(ratio);
io:println(m.get("factor"));
}
Note how the first get
call panics, causing abrupt termination of the program.
$ bal run sample.bal
error: {ballerina/lang.map}KeyNotFound {"message":"cannot find key 'ratio'"}
at ballerina.lang.map.0:get(map.bal:84)
sample:main(sample.bal:10)
Now let’s use trap
with this expression that may panic. When used in a trap
expression, the resultant type of trap m.get(k)
would be the union of the type of the expression (m.get(k)
here), which is int|string
, and error
.
import ballerina/io;
public function main() {
map<int|string> m = {
factor: 5,
'default: 2,
name: "def"
};
// The type of `ratio` should now include `error`.
int|string|error ratio = trap m.get("ratio");
io:println(ratio);
io:println(m.get("factor"));
}
$ bal run sample.bal
error("{ballerina/lang.map}KeyNotFound",message="cannot find key 'ratio'")
5
Note how the program does not terminate abruptly now, even though the first get
call panics.
Although we haven’t used them yet, it is possible to define subtypes of errors, as we will see in a different article. With these custom subtypes, functions can have more refined return types rather than the generic error
type. However, with the trap
expression, the static type will always be the union with the builtin error
type since it is not possible to identify the exact errors an expression (or action) may panic with by looking at the type of the expression.
This was a simple introduction to the different concepts and constructs associated with the error
type and error values in Ballerina. In a few other articles we will look at how we can define and work with custom errors, destructuring and matching error values, etc.
Also see — official examples on error handling in Ballerina
This article is an updated version of [Ballerina] Error Handling — Part I, which was written based on an older version of Ballerina.
Also published on Stack Overflow.