In this small post we see different error handling strategies that Odin provides and compare it to Elixir.
Elixir
In Elixir we can handle errors by returning a tuple {:error, reason}
and {:ok, result}
if the result is ok. Also we can have exceptions (try, catch, rescue).
defmodule PositiveSum do
def sum(a, b) when is_number(a) and is_number(b) do
case (a + b) do
result when result > 0 -> {:ok, result}
result when result < 0 -> {:error, "Only positive results allowed"}
_ -> {:error, "Zero is neither positive nor negative"}
end
end
end
PositiveSum.sum(1, 2)
|> IO.inspect
PositiveSum.sum(1, -4)
|> IO.inspect
PositiveSum.sum(0, 0)
|> IO.inspect
try do
PositiveSum.sum(1, "b")
rescue
_error -> {:error,"Params are not numbers"}
end
|> IO.inspect
Output
{:ok, 3}
{:error, "Only positive results allowed"}
{:error, "Zero is neither positive nor negative"}
{:error, "Params are not numbers"}
Odin
In Odin we can handle the errors using different strategies by returning multiple results from a procedure. Since Odin is a typed language, is a lot harder to send wrong typed params to a procedure. It won’t compile.
Strategy 1: ok
booleans
The first strategy is to return a bool
at the last return parameter. This strategy will only give a ok
or !ok
status.
sum := proc(a : int, b : int) -> (result: int, ok: bool) {
result = a + b
if result > 0 {
return result, true
}
if result < 0 {
return result, false
}
return result, false
}
fmt.println(sum(1, 2)) // 3, true
fmt.println(sum(1, -4)) // -3, false
fmt.println(sum(0, 0)) // 0, false
fmt.println(sum(0, "b")) // won't compile
But if we use the result, we would need to store it in several variables. Or we would get a compilation error similar to: Error: Assignment count mismatch '1' = '2'
.
result := sum(1, -4) // Won' compile.
This means we must store the success
(ok
) status somewhere.
// -3, false
result, ok := sum(1, -4)
if !ok {
fmt.printfln("%d, %s", result, "There were problems in the Sum")
}
We could ommit the variable using the special _
character (rune).
// -3, false
result, _ := sum(1, -4)
fmt.printfln("%d", result)
We can add #optional_ok
tag to the procedure declaration so we can omit the final boolean.
Important
#optional_ok
requires exactly 2
return params. Only accepts the last param as a boolean.
sum := proc(a : int, b : int) -> (result: int, ok: bool) #optional_ok {...}
// -3, false
result := sum(1, -4)
fmt.printfln("%d", result)
There is also the #optional_allocation_error
that can be used instead of #optional_ok
and its meant for procedures that could return an allocation error.
Strategy 2: Error messages
We can return the message. However there is no way to tag the declaration to be optional the same way a boolean can.
sum :: proc(a : int, b : int) -> (result: int, err: string) {
result = a + b
if result > 0 {
return result, ""
}
if result < 0 {
return result, "Only positive results allowed"
}
return result, "Zero is neither positive nor negative"
}
result, err := sum(5, -6)
if err != "" {
fmt.printfln("%d, %s", result, err)
}
Strategy 3: Int returns
This can be used when dealing with C libraries or system processes that returns a number to indicate status. We have to combine them with arrays or enums so we can have an error message.
error_strings := [?]string{
"ok",
"Only positive results allowed",
"Zero is neither positive nor negative"
}
sum :: proc(a : int, b : int) -> (result: int, err: int) {
result = a + b
if result > 0 {
return result, 0
}
if result < 0 {
return result, 1
}
return result, 2
}
result, status := sum(5, -6)
fmt.printfln("%d, %s", result, error_strings[status])
Strategy 4: Enum Errors
This strategy provides a little more standarization of error codes and messages, by using enum
.
PositiveSumError :: enum {
None,
Negative_Result,
Zero_Result,
}
positive_sum_error_message :: proc(err : PositiveSumError) -> (message: string) {
switch err {
case .None:
message = ""
case .Negative_Result:
message = "Only positive results allowed"
case .Zero_Result:
message = "Zero is neither positive nor negative"
}
return message
}
sum :: proc(a : int, b : int) -> (result: int, err: PositiveSumError) {
result = a + b
if result > 0 {
return result, .None
}
if result < 0 {
return result, .Negative_Result
}
return result, .Zero_Result
}
result, status := sum(5, -6)
fmt.printfln("%d, %v", result, positive_sum_error_message(status))
This can also be simplified to
PositiveSumError :: enum {
None,
Negative_Result,
Zero_Result,
}
// usage: error_strings[.Negative_Result]
error_strings := [PositiveSumError]string{
.None = "ok",
.Negative_Result = "Only positive results allowed",
.Zero_Result = "Zero is neither positive nor negative"
}
You can use unions
to join different enums
. We can see an example in net/common.odin
.
General_Error :: enum u32 {
None = 0,
Unable_To_Enumerate_Network_Interfaces = 1,
}
DNS_Error :: enum u32 {
Invalid_Hostname_Error = 1,
Invalid_Hosts_Config_Error,
Invalid_Resolv_Config_Error,
Connection_Error,
Server_Error,
System_Error,
}
Network_Error :: union #shared_nil {
General_Error,
DNS_Error,
// ... //
}
Strategy 5: Struct Errors
In this strategy we use structs
to save the message. Optionally we combine it with enums
for easier comparison later.
PositiveSumErrorCode :: enum {
None,
Negative_Result,
Zero_Result,
}
PositiveSumError :: struct {
message: string,
code : PositiveSumErrorCode,
}
sum :: proc(a : int, b : int) -> (result: int, err: PositiveSumError) {
result = a + b
if result > 0 {
return result, PositiveSumError{code = .None}
}
if result < 0 {
return result, PositiveSumError{
message = "Only positive results allowed",
code = .Negative_Result,
}
}
return result, PositiveSumError{
message = "Zero is neither positive nor negative",
code = .Zero_Result,
}
}
result, err := sum(5, 6)
fmt.printfln("%d, %s", result, err.message)
Strategy 6: Struct + Ok
In this strategy we combine both the struct
errors and ok
boolean. This is the most similar to Elixir and other programming languages that uses exceptions
.
PositiveSumErrorCode :: enum {
None,
Negative_Result,
Zero_Result,
}
PositiveSumError :: struct {
message: string,
code : PositiveSumErrorCode,
}
PositiveSumResult :: struct {
value : int,
error : PositiveSumError,
}
sum :: proc(a : int, b : int) -> (result: PositiveSumResult, ok : bool) #optional_ok {
value := a + b
if value > 0 {
return PositiveSumResult{
value = value,
error = PositiveSumError{code = .None},
}, true
}
if value < 0 {
return PositiveSumResult{
value = value,
error = PositiveSumError{
message = "Only positive results allowed",
code = .Negative_Result,
},
}, false
}
return PositiveSumResult{
value = value,
error = PositiveSumError{
message = "Zero is neither positive nor negative",
code = .Zero_Result,
},
}, false
}
result, ok := sum(5, -6)
// -1, Only positive results allowed, ok? false
fmt.printfln("%d, %s, ok? %v", result.value, result.error.message, ok)