Skip to content

Commit

Permalink
Merge pull request #5709 from heshanpadmasiri/feat/reword-error-doc
Browse files Browse the repository at this point in the history
Improve Error Handling related BBEs
  • Loading branch information
gimantha authored Oct 14, 2024
2 parents 96319c9 + 686a4ae commit 7bf8e03
Show file tree
Hide file tree
Showing 18 changed files with 244 additions and 86 deletions.
22 changes: 14 additions & 8 deletions examples/check-expression/check_expression.bal
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import ballerina/io;

// Convert `bytes` to a `string` value and then to an `int` value.
function intFromBytes(byte[] bytes) returns int|error {
string|error res = string:fromBytes(bytes);
// Explicitly check if the result is an error and
// immediately return if so.
if res is error {
return res;
}
return int:fromString(res);
}

// Use `check` with an expression that may return `error`.
// If `string:fromBytes(bytes)` returns an `error` value, `check`
// makes the function return the `error` value here.
// If not, the returned `string` value is used as the value of the `str` variable.
// Same as `intFromBytes` but with `check` instead of explicitly checking for error and returning.
function intFromBytesWithCheck(byte[] bytes) returns int|error {
string str = check string:fromBytes(bytes);

return int:fromString(str);
}

public function main() {
int|error res = intFromBytes([104, 101, 108, 108, 111]);
io:println(res);
int|error res1 = intFromBytesWithCheck([104, 101, 108, 108, 111]);
io:println(res1);
int|error res2 = intFromBytes([104, 101, 108, 108, 111]);
io:println(res2);
}
8 changes: 4 additions & 4 deletions examples/check-expression/check_expression.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Check expression

`check E` is used with an expression `E` that might result in an `error` value. If `E` results in an `error` value , then, `check` makes the function return that `error` value immediately.

The type of `check E` does not include `error`. The control flow remains explicit.
If an expression can evaluate to an error value, you can use the `check` expression to indicate that you want the execution of the current block to terminate with that error as the result. This results in the error value being returned from the current function/worker, unless the `check` expression is used in a failure-handling statement (e.g., statement with on fail, retry statement).

::: code check_expression.bal :::

::: out check_expression.out :::
::: out check_expression.out :::

+ [`check` semantics](https://ballerina.io/learn/concurrency/#check-semantics)
1 change: 1 addition & 0 deletions examples/check-expression/check_expression.out
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
$ bal run check_expression.bal
error("{ballerina/lang.int}NumberParsingError",message="'string' value 'hello' cannot be converted to 'int'")
error("{ballerina/lang.int}NumberParsingError",message="'string' value 'hello' cannot be converted to 'int'")
64 changes: 45 additions & 19 deletions examples/error-reporting/error_reporting.bal
Original file line number Diff line number Diff line change
@@ -1,31 +1,57 @@
import ballerina/io;

// Parses a `string` value to convert to an `int` value. This function may return error values.
// The return type is a union with `error`.
function parse(string s) returns int|error {
type Person record {
string name;
int age;
};

int n = 0;
int[] cps = s.toCodePointInts();
foreach int cp in cps {
int p = cp - 0x30;
if p < 0 || p > 9 {
// If `p` is not a digit construct, it returns an `error` value with `not a digit` as the error message.
return error("not a digit");
function validatePeople(Person[] people) returns error? {
if people.length() == 0 {
// Create an error value specifying only the error message.
return error("Expected a non-empty array");
}

foreach Person p in people {
io:println("Validating ", p.name);
error? err = validatePerson(p);
if err is error {
// Create a new error value with the validation error as the cause
// and the `Person` value for which validation failed in the detail mapping.
return error("Validation failed for a person", err, person = p);
}
n = n * 10 + p;
}
return n;
}

public function main() {
// An `int` value is returned when the argument is a `string` value, which can be parsed as an integer.
int|error x = parse("123");
function validatePerson(Person person) returns error? {
int age = person.age;
if age < 0 {
// If validation fails for age, create a new error value specifying
// an error message and the age value for which validation failed.
return error("Age cannot be negative", age = age);
}
}

io:println(x);
public function main() {
Person[] people = [
{name: "Alice", age: 25},
{name: "Bob", age: -1},
{name: "Charlie", age: 30}
];
error? err = validatePeople(people);
if err is error {
printError(err);
}
}

// An `error` value is returned when the argument is a `string` value, which has a character that is not a digit.
int|error y = parse("1h");
// Helper function to print internals of error value.
function printError(error err) {
io:println("Message: ", err.message());
io:println("Detail: ", err.detail());
io:println("Stack trace: ", err.stackTrace());

io:println(y);
error? cause = err.cause();
if cause is error {
io:println("Cause:");
printError(cause);
}
}
18 changes: 14 additions & 4 deletions examples/error-reporting/error_reporting.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
# Errors

Ballerina does not have exceptions. Errors are reported by functions returning `error` values.
`error` is its own basic type. The return type of a function that may return an `error` value will be a union with `error`.
Ballerina does not have exceptions. Instead functions report invalid states by returning error values. Each error value has,
1. Message, a human-readable `string` value describing the error.
2. Cause, which is an `error` value if this error was caused due to another error, which needs to be propagated, otherwise nil.
3. Detail, a mapping value consisting of additional information about the error.
4. Stack trace, a snapshot of the state of the execution stack when the error value was created.

An `error` value includes a `string` message. An `error` value includes the stack trace from the point at which the error is constructed (i.e., `error(msg)` is called). Error values are immutable.
Error values are immutable.

You can create a new error value using an error constructor. As the first argument to the error constructor, it expects the message string. As the second argument, you can optionally pass in an `error?` value for cause. Subsequent named arguments, if specified, will be used to create the detail mapping. The stack trace is provided by the runtime.

::: code error_reporting.bal :::

::: out error_reporting.out :::
::: out error_reporting.out :::

## Related links
- [Error subtyping](https://ballerina.io/learn/by-example/error-subtyping/)
- [Error cause](https://ballerina.io/learn/by-example/error-cause/)
- [Error detail](https://ballerina.io/learn/by-example/error-detail/)
11 changes: 9 additions & 2 deletions examples/error-reporting/error_reporting.out
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
$ bal run error_reporting.bal
123
error("not a digit")
Validating Alice
Validating Bob
Message: Validation failed for a person
Detail: {"person":{"name":"Bob","age":-1}}
Stack trace: [callableName: validatePeople fileName: error_reporting.bal lineNumber: 20,callableName: main fileName: error_reporting.bal lineNumber: 40]
Cause:
Message: Age cannot be negative
Detail: {"age":-1}
Stack trace: [callableName: validatePerson fileName: error_reporting.bal lineNumber: 30,callableName: validatePeople fileName: error_reporting.bal lineNumber: 16,callableName: main fileName: error_reporting.bal lineNumber: 40]
56 changes: 44 additions & 12 deletions examples/error-subtyping/error_subtyping.bal
Original file line number Diff line number Diff line change
@@ -1,21 +1,53 @@
import ballerina/io;

// `distinct` creates a new subtype.
type XErr distinct error;
type YErr distinct error;
type InvalidIntDetail record {|
int value;
|};

type Err XErr|YErr;
// Subtype of `InvalidIntDetail`.
type InvalidI32Detail record {|
int:Signed32 value;
|};

// The name of the distinct type can be used with the `error` constructor to create an error value
// of that type. `err` holds an `error` value of type `XErr`.
Err err = error XErr("Whoops!");
// Error with the `InvalidIntDetail` type as the detail type.
type InvalidIntError error<InvalidIntDetail>;

function desc(Err err) returns string {
// The `is` operator can be used to distinguish distinct subtypes.
return err is XErr ? "X" : "Y";
// `error` with `InvalidI32Detail` as the detail type. Thus it is a subtype of `InvalidIntError`.
type InvalidI32Error error<InvalidI32Detail>;

}
// Distinct error with the `InvalidIntDetail` type as the detail type and a unique type ID.
// Therefore, this is a proper subtype of `InvalidIntError`, but doesn't have a subtype relationship
// with `AnotherDistinctIntError` because they have different type IDs.
type DistinctIntError distinct error<InvalidIntDetail>;

// Another `error` with `InvalidIntDetail` as the detail type and different type ID to `DistinctIntError`
// This is also a proper subtype of `InvalidIntError`, but doesn't have a subtype relationship with `DistinctIntError`
type AnotherDistinctIntError distinct error<InvalidIntDetail>;

public function main() {
io:println(desc(err));
InvalidI32Error e1 = createInvalidI32Error(5);
io:println(e1 is InvalidIntError);

InvalidIntError e2 = createInvalidIntError(5);
io:println(e2 is DistinctIntError);

DistinctIntError e3 = createDistinctInvalidIntError(5);
// This is true because `InvalidInt` is not a distinct type, thus it ignores the type id of `e3`.
io:println(e3 is InvalidIntError);

// This is false because `DistinctIntError` and `AnotherDistinctIntError` have different type ids.
io:println(e3 is AnotherDistinctIntError);
}

// Helper functions to create errors.
function createInvalidIntError(int value) returns InvalidIntError {
return error("Invalid int", value = value);
}

function createDistinctInvalidIntError(int value) returns DistinctIntError {
return error("Invalid int", value = value);
}

function createInvalidI32Error(int:Signed32 value) returns InvalidI32Error {
return error("Invalid i32", value = value);
}
13 changes: 10 additions & 3 deletions examples/error-subtyping/error_subtyping.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
# Error subtyping

`distinct` creates a new subtype and can be used to define subtypes of `error`. The name of the distinct `error` type can be used with the error constructor to create an `error` value of that type.
If we want to identify if a given `error` type (say `ESub`) is a subtype of another error type (say `ESuper`), first we need to check if `ESuper` is a distinct error type. If it is not then `ESub` is a subtype if and only if the detail type of `ESub` is a subtype of the detail type of `ESuper`.

Works like a nominal type. The `is` operator can be used to distinguish distinct subtypes. Each occurrence of `distinct` has a unique identifier that is used to tag instances of the type.
If more explicit control over error type relations is desired you can use `distinct` error types. Each declaration of a distinct error type has a unique type ID. If `ESuper` is a distinct error type there is the additional requirement that type ID of `ESub` must belong to the type ID set of `ESuper` for it to be a subtype.

Note that you can create subtypes of distinct error types by intersecting them with other error types.

```ballerina
::: code error_subtyping.bal :::
::: out error_subtyping.out :::
::: out error_subtyping.out :::
## Related links
- [Type intersection for error types](https://ballerina.io/learn/by-example/error-type-intersection/)
5 changes: 4 additions & 1 deletion examples/error-subtyping/error_subtyping.out
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
$ bal run error_subtyping.bal
X
true
false
true
false
66 changes: 40 additions & 26 deletions examples/error-type-intersection/error_type_intersection.bal
Original file line number Diff line number Diff line change
@@ -1,33 +1,47 @@
import ballerina/io;

type IOError distinct error;
type InputErrorDetail record {|
int|string value;
|};

type FileErrorDetail record {
string filename;
};
type NumericErrorDetail record {|
int|float value;
|};

// The `FileIOError` type is defined as an intersection type using the `&` notation.
// It is the intersection of two error types: `IOError` and `error<FileErrorDetail>`.
// An error value belongs to this type if and only if it belongs to both `IOError`
// and `error<FileErrorDetail>`.
type FileIOError IOError & error<FileErrorDetail>;
type InputError error<InputErrorDetail>;

type NumericError error<NumericErrorDetail>;

type DistinctInputError distinct error<InputErrorDetail>;

type DistinctNumericError distinct error<NumericErrorDetail>;

// `NumericInputError` has detail type, `record {| int value |}`.
type NumericInputError InputError & NumericError;

// `DistinctNumericInputError` has type ids of both `DistinctInputError` and `DistinctNumericError`.
type DistinctNumericInputError DistinctInputError & DistinctNumericError;

function createNumericInputError(int value) returns NumericInputError {
return error("Numeric input error", value = value);
}

function createDistinctNumericInputError(int value) returns DistinctNumericInputError {
return error("Distinct numeric input error", value = value);
}

public function main() {
// In order to create an error value that belongs to `FileIOError`, the `filename`
// detail field must be provided.
FileIOError fileIOError = error("file not found", filename = "test.txt");

// `fileIOError` belongs to both `IOError` and `error<FileErrorDetail>`.
io:println(fileIOError is IOError);
io:println(fileIOError is error<FileErrorDetail>);

// An `IOError` value will not belong to `FileIOError` if it doesn't belong to
// `error<FileErrorDetail>`.
IOError ioError = error("invalid input");
io:println(ioError is FileIOError);

// Similarly, an error value belonging to `error<FileErrorDetail>` will not belong
// to `FileIOError` if it doesn't belong to `IOError`.
error<FileErrorDetail> fileError = error("cannot remove file", filename = "test.txt");
io:println(fileError is FileIOError);
NumericInputError e1 = createNumericInputError(5);
// `e1` belong to `InputError` since its detail type is a subtype of `InputErrorDetail`.
io:println(e1 is InputError);

// `e1` doesn't belong to `DistinctInputError` since it doesn't have the type id of `DistinctInputError`.
io:println(e1 is DistinctInputError);

DistinctNumericInputError e2 = createDistinctNumericInputError(5);
// `e2` belong to `InputError` since it's detail type is a subtype of `InputErrorDetail`.
io:println(e2 is InputError);

// `e2` belong to `DistinctInputError` since it's type id set include the type id of `DistinctInputError`.
io:println(e2 is DistinctInputError);
}
4 changes: 3 additions & 1 deletion examples/error-type-intersection/error_type_intersection.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Type intersection for error types

You can define an error type that is both a subtype of a `distinct` error type and has additional constraints on the detail fields using intersection types.
If you intersect two `error` types, the resulting type's detail type is the intersection of the detail types of both types. Furthermore, if any of the types being intersected is a distinct type, then the resultant type's type ID set includes all the type IDs of that type. Thus it is a subtype of both types and this is how you create subtypes of `distinct` error types.

::: code error_type_intersection.bal :::

::: out error_type_intersection.out :::

+ [Error subtyping](https://ballerina.io/learn/by-example/error-subtyping/)
4 changes: 2 additions & 2 deletions examples/error-type-intersection/error_type_intersection.out
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
$ bal run error_type_intersection.bal
true
true
false
false
true
true
7 changes: 7 additions & 0 deletions examples/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,13 @@
"verifyOutput": true,
"isLearnByExample": true
},
{
"name": "Trap expression",
"url": "trap-expression",
"verifyBuild": true,
"verifyOutput": true,
"isLearnByExample": true
},
{
"name": "Type intersection for error types",
"url": "error-type-intersection",
Expand Down
6 changes: 2 additions & 4 deletions examples/panics/panics.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# Panics

Ballerina distinguishes normal errors from abnormal errors. Normal errors are handled by returning error values. Abnormal errors are handled using the panic statement. Abnormal errors should typically result in immediate program termination.

E.g., A programming bug or out of memory. A panic has an associated error value.
Ballerina distinguishes normal errors from abnormal errors. Normal errors are handled by returning error values. This signals to the caller that they must handle the error. In contrast, abnormal errors such as division by 0 and out-of-memory are typically unrecoverable errors and we need to terminate the execution of the program. This is achieved by a panic statement with an expression that results in an error value such as the error constructor. At runtime, evaluating a panic statement first creates the error value by evaluating the expression and then starts stack unwinding, unless it encounters a `trap` expression.

::: code panics.bal :::

::: out panics.out :::
::: out panics.out :::
Loading

0 comments on commit 7bf8e03

Please sign in to comment.