Error Handling
In Fuse we do not like code that may raise an unexpected error, That’s why we try to push errors to the type system. To achieve such a goal we are using Result
and Optional
types instead of throw
and nil
; There is also a try
expression which in itself is very very similar to the try ... catch
pattern but because of special type treatment we can mark functions that can throw and enforce error handling for any function.
Result
The Result
union is used to return the result of an explicit error. It can either be Ok
or Err
.
fn network_call(url: string) -> Result<string>
if not validate_url(url) then
return Err("Invalid URL")
elseif not ping(url) then
return Err('Failed to ping the "${url}"')
end
Ok(fetch(url))
end
Note: Notice that we do not need to annotate the full name of Err
and Ok
(eg: Result<string>::Ok
) since they both have global aliases by default.
A Result
then can be used to lift the value, It is possible either by an unwrap
operation or the use of pattern matching.
let result = network_call(url)
-- ...
if result.is_ok() then
print(result.unwrap())
else
print(result.err())
end
-- or
print(result.unwrap_or("default value"))
-- or
match result when
Ok(r) then print(r) end
Err(e) then print(e) end
end
There are more ways to perform a lift operation on a Result
, Learn more about it in the reference for the Result
type.
Optional
There are times when we want to define a function that may or may not return a value due to a runtime error, a check failure, or some other documented behavior. The Optional
type describes this exact situation as a type. An Optional
value is similar to a value that can either be a nil
or a value, It is compiled to the same thing we just have extra type information for the compiler so it can reason about nilable
values easier.
Note: The Optional type is also known as Option
, Maybe
, and Nullable
in other programming languages.
fn load_config() -> Optional<Config>
if config_exists() then
Some(read_config())
else
None
end
end
It can get unwrapped just like a Result
value.
let opt = load_config()
if opt.is_some() then
print(opt.unwrap())
else
print("No config!")
end
-- or
print(opt.unwrap_or("No config!"))
-- or
match opt when
Some(conf) then print(conf) end
None then print("No config!") end
end
Try expression
All of the Fuse libraries are created using the solutions mentioned above. We still allow unsafe
functions in our code, By default all modules imported from the Lua path(@lua
) are imported as unsafe
unless they have a type definition(declare
) that defines them as otherwise.
We can call these unsafe functions by putting a try
keyword before them.
unsafe fn unsafe_func()
-- ...
end
fn safe_func()
try unsafe_func() -- OK
unsafe_func() -- Compiler Error
end
The try
is an expression which means there is no try ... catch
system, It will try to execute the expression and if it throws an error it will trap that and return the value or error as a Result
union.
unsafe fn unsafe_func(x: unsafe, y: number) -> number
-- ...
end
fn safe_func()
let result: Result<number> = try unsafe_func()
end
A try block can also be used to execute multiple unsafe
operations.
fn safe_func()
let result: Result<number> = try do
let a = unsafe_func1()
let b = unsafe_func2()
a + b
end
end
Note: Using a try block in situations where the specific error origin isn’t important is a better choice for performance.
If we don’t try
an unsafe function similar to async-await
we have to also mark our function as unsafe
so somewhere higher up the function chain we finally try everything together.
unsafe fn a()
-- ...
end
unsafe fn b()
-- ...
end
unsafe fn c()
a + b
end
unsafe fn d()
let value = c()
let a = a()
let b = b()
(a, b, c)
end
fn safe_func()
let (a, b, c) = try d()
end
- Previous
- Next