6. Absence and failure
Base distinguishes three situations that other languages often handle the same way:
- A value may be absent. A search may find nothing. Its type is
T?, an optional, where Python would returnNone. - An operation may fail. A file may not open; text may not parse. A function that can fail says so in its result type,
T!, where Python would raise an exception. - The program has a bug. An index is out of range, an addition overflowed. The program stops with a trap, which cannot be caught.
Optionals#
func digit_value(c: char) -> u32?:
if c >= '0' and c <= '9':
return (u32)c - (u32)'0'
return none
pub func main(arguments: str[]) -> i32:
if let d = digit_value('7'): print(f"digit {d}")
let missing = digit_value('x') else 0
print(f"fallback {missing}")
match digit_value('3'):
.some(n): print(f"some {n}")
.none: print("none")
return 0digit 7 fallback 0 some 3
A u32? holds either a u32 or none. Unlike Python, where a None can reach code that expected a number, an optional has to be unwrapped before the value inside can be used, and each way of unwrapping says what happens when it is empty:
if let name = optional:runs the block only when there is a value, and names it. This is Swift'sif let.optional else fallbackgives the value, or the fallback, like Python'sx or defaultbut only fornone. The fallback can also leave:else return,else break,else error(...), orelse trap("reason").while let item = next():repeats while there is a value.matchwith the cases.some(value)and.none.
To assert that a value cannot be missing, write else trap("the reason"). There is no operator that unwraps without saying why.
Failure#
A function that can fail puts ! after its result type, and error(code, message) makes it fail. Error codes are constants, declared with ErrorCode.package(n):
let not_a_number = ErrorCode.package(1)
func digit_value(c: char) -> u32?:
if c >= '0' and c <= '9':
return (u32)c - (u32)'0'
return none
func parse(text: str) -> u32!:
if text.length == 0:
error(not_a_number, "empty text")
var value: u32 = 0
for c in text:
let digit = digit_value(c) else error(not_a_number, "not a digit")
value = value * 10 + digit
return value
func half(text: str) -> u32!:
let n = try parse(text)
return n // 2
pub func main(arguments: str[]) -> i32:
let a = half("84") else 0
print(f"half of 84 is {a}")
let b = parse("8a") catch failure:
print(f"could not parse: {failure.message}")
recover 0
print(f"8a parsed as {b}")
return 0half of 84 is 42 could not parse: not a digit 8a parsed as 0
Where Python lets an exception travel up silently, Base makes each call that can fail say how the failure is handled. There are three ways:
trypasses it on.try parse(text)gives the value, or makes the current function fail with the same error, which therefore must itself be marked!. This is Rust's?operator, or Swift'stry. Onetrycovers a whole expression:try combine(read(), parse())needs only one.catchhandles it, like Python'sexcept.expression catch failure:runs the block with the error, whose fields arefailure.code,failure.messageandfailure.at, where it was raised, withfailure.called_at, the call in this code that an error from a library came out of. The block must end by giving a replacement value withrecover, or by leaving:return,error(...)ortrap(...).elsereplaces it, as it replaces a missing optional:parse(text) else 0.
Ignoring a failure is not allowed: calling parse(text) without one of these is a compile error.
Constructors and main can fail too#
A struct's own init can fail, and so can main when it returns i32!. An error that reaches the end of main is printed and the program exits with status 1:
let out_of_range = ErrorCode.package(2)
struct Percentage:
let value: u32
func init(value: u32) -> !:
if value > 100:
error(out_of_range, "a percentage is at most 100")
self.value = value
pub func main(arguments: str[]) -> i32!:
let p = try Percentage(75)
print(f"{p.value}%")
let q = try Percentage(150)
print(f"{q.value}%")
return 0error: main.lucb:8:13: a percentage is at most 100 (code 2)
The output above is standard error; 75% went to standard output. main.lucb:8:13 is the error(...) statement that raised the failure, as a Python traceback ends at the raise. -> ! on init means "returns nothing, but may fail". The code printed is the number given to ErrorCode.package.
Cleaning up#
When a function sets several things up and fails halfway, the earlier ones need undoing. defer runs a statement when the current block ends, however it ends, much like Python's finally; errdefer runs it only when the block is left because of an error. Chapter 7 uses both to free memory.
Traps#
A trap reports a bug and stops the program. These trap, in every build: integer overflow with +, - or *; division by zero; an index or slice out of range; a checked conversion that does not fit; else trap(...); a failed assert(condition); and running out of stack. Each prints trap: file:line:column: reason and exits with status 1. A trap cannot be caught, and defer does not run.
Where next#
Chapter 7 covers where values live, and memory you manage yourself.