Reference / Errors and Result
Errors and Result
A function that can fail says so in its return type. The type T | E holds a success of the type T, or a failure of the error type E. A value of that type is a result.
fn parse(text: string) -> i64 | string {
i64(text) ?? fail "not a number: {text}"
}
fn double(text: string) -> i64 | string {
let n = parse(text)?
n * 2
}
fn main() {
print(double("21") ?? 0) // 42
print(double("x") ?? 0) // 0
}
A failure passes up one visible step at a time: fail makes it, ? passes it on, and ??, if let and match handle it. Sisu has no exceptions. In the example, i64(text) is None when the text is not a number (Parsing a string).
Results In development
T | E is short for the type Result<T, E> (Option and Result). Both spellings are the same type. A message from sisu writes the type with |.
enum PortError: Error {
Empty
NotANumber(text: string)
fn to_string(self) -> string {
match self {
Empty => "the port is empty"
NotANumber(text) => "{text} is not a number"
}
}
}
fn port(text: string) -> i64 | PortError {
if text == "" {
fail PortError.Empty
}
i64(text) ?? fail PortError.NotANumber(text: text)
}
fn save(path: string, text: string) -> unit | IoError {
write_file(path, text)
}
fn main() {
let ports: [i64 | PortError] = [port("80"), port("")]
print(ports.len()) // 2
}
The rules:
- The left side is the success type. It can be any type, also
unitand an error type. The right side is the error type. - The error type must conform to the interface
Error. So-> i64 | boolis an error. The interfaceErrorrequiresDisplay. stringconforms toError. So-> i64 | stringis a function that fails with a message.- A function that returns no value on success returns
unit | E. There is no formfn save(...) | IoError. - A result has one error type.
T | A | Bis an error. |in a type never means "one of these types".i64 | stringis ani64or a failure message, andi64 | booldoes not build.i64 | E?isi64 | (E?), and its right side is not an error type. For an optional result, write(i64 | E)?.- A generic function that returns
T | Eneeds the boundE: Error.Result<i64, bool>is the same error asi64 | bool. - An alias can name a result with a fixed error type:
type IoResult<T> = T | IoError. - A program cannot declare its own top-level
Error.
A plain value is the success In development
Where a T | E is expected, a value of type T becomes the success. A function body that ends with a value returns it as a success. A unit | E body that reaches its end succeeds.
fn size(name: string) -> i64 | string {
if name == "" {
fail "no name"
}
name.len()
}
fn log(text: string) -> unit | string {
print(text)
}
fn main() {
print(size("sisu") ?? 0) // 4
print(size("") ?? 0) // 0
let _ = log("done") // done
}
The rules:
- A value of the error type
Edoes not become a failure. A failure comes fromfail, or fromErr(e)where a result is expected. Ok(x)andErr(e)are valid expressions. Code needs them mostly in patterns.
fail In development
fail e returns Err(e) from the enclosing function.
enum ConfigError: Error {
BadLine(line: i64)
}
fn check(line: i64, ok: bool) -> unit | ConfigError {
if !ok {
fail ConfigError.BadLine(line: line)
}
}
fn main() {
match check(2, false) {
Ok(_) => print("ok")
Err(_) => print("failed") // failed
}
}
fail is a keyword. An expression fail e has the type never, as a call of panic does. So it can be a branch of an if or a match, an arm, and the right side of ?? (Types).
The ? operator In development
x? passes a failure up. When x is a T | E that holds a success, x? is the value of type T. When x holds a failure, the enclosing function returns that failure.
fn length(path: string) -> i64 | IoError {
let text = read_file(path)?
text.len()
}
fn main() -> unit | IoError {
print(length("notes.txt")?)
}
The rules:
- The return type of the enclosing function must be a result
U | F. WhenFisE, the failure passes up as it is. read_file(path)?also works in a function that returns aT | string.- On an optional
T?,x?is the value whenxholds one. WhenxisNone, the enclosing function returnsNone. Its return type must be an optional. ?on a result in a function that returns an optional is an error. So is?on an optional in a function that returns a result. Writex ?? fail ...to turn an empty optional into a failure.?works on a result or an optional only.5?is an error.?is a postfix operator and binds as a call does.x??is the operator??. To apply?twice, write(x?)?.- On a result,
x?.namepasses a failure up, then reads the member:read_file(p)?.len()is(read_file(p)?).len(). On an optional,?.is still optional chaining. x?calculatesxonce. When it is a failure, nothing after it in the same statement runs.- A
?followed by a., with white space or a line break between them, means?.. Soopt? .len()meansopt?.len().
In a closure In development
In a closure, fail and ? act on the return type of the closure, as return does. A failure ends the closure, and the function around it goes on.
fn parse(text: string) -> i64 | string {
i64(text) ?? fail "not a number: {text}"
}
fn main() {
let check: (string) -> bool | string = text => {
let n = parse(text)?
n > 0
}
if let Ok(positive) = check("12x") {
print(positive)
}
print("main goes on") // main goes on
}
Handling a failure In development
r ?? d gives the success of the result r. On any failure it gives d. The program calculates d only on a failure. Its type is T, or never, as a call of panic has.
enum LoadError: Error {
Missing
}
fn load(name: string) -> string | LoadError {
if name == "" {
fail LoadError.Missing
}
name
}
fn main() {
print(load("") ?? "default") // default
print(load("sisu") ?? panic("must load")) // sisu
if let Ok(text) = load("sauna") {
print(text) // sauna
}
match load("") {
Ok(text) => print(text)
Err(_) => print("failed") // failed
}
}
The rules:
if letandwhile leton a result nameOk(...)orErr(...):if let Ok(config) = load(...). The pattern shows that the program lets a failure go. A short pattern,if let config = load(...), is an error.- On an optional, the short form of
if letstays. - A
matchon a result works as on any enum, with the check that it covers every value. - A
matchwith the armsOk(Some(x)),Ok(None)andErr(e)tells the end of the input from a failure. - To pass the failure up instead, apply
?to the operand:while let line = next()?.
An unused result In development
A result that the program does not use is an error. It replaces the warning for an unused value (W0004). To ignore a result on purpose, write let _ = e.
fn save(path: string, text: string) -> unit | IoError {
write_file(path, text)
}
fn main() {
// The result of `save` may be a failure and is not used.
save("out.txt", "text")
// This ignores it on purpose.
let _ = save("out.txt", "text")
}
The last expression of a function body is its value, so a unit | E body that ends in save(...) returns that result. It is used.
A main that fails In development
main can return a unit | E. When it returns a failure, the program writes error: and the text of the error to standard error, and ends with the exit code 1.
fn parse(text: string) -> i64 | string {
i64(text) ?? fail "not a number: {text}"
}
fn main() -> unit | string {
let n = parse("12x")?
print(n)
}
The program above writes error: not a number: 12x to standard error. A panic still ends the program with the exit code 101.
main with any other result type, or with a type other than unit, is an error (E0029).
Files In development
read_file and write_file read and write a whole file. A failure names the path.
fn main() -> unit | IoError {
let text = read_file("notes.txt")?
write_file("copy.txt", text)
}
| Function | Result |
|---|---|
read_file(path) |
The text of the file, a string | IoError |
write_file(path, text) |
A unit | IoError. It creates the file or replaces its content |
IoError is an enum that conforms to Error. It has the variants NotFound(path), PermissionDenied(path), NotUtf8(path) and Other(path, reason). A path with a NUL character fails with Other and the reason the path contains a NUL character, before any system call.
write_file is not atomic. A failure part way can leave a partial file. A program cannot declare its own top-level read_file, write_file or IoError.
These functions give a program no file handle. A program cannot append to a file, read it line by line or move inside it. The functions print, eprint and read_line keep their rules (Input and output).
Sisu