Handling Errors
Polar has throw and
try/catch, like JavaScript, with one
difference: the errors a function can throw are part of its type, so
nothing is thrown that the caller doesn't know about.
Throwing an error
An error is an ordinary value, usually a variant case that describes what went wrong:
types
Missing = Missing(String)
functions
balance(name: String) -> Int / {Throws<Missing>} {
match name {
"ann" -> 120,
"bob" -> 15,
_ -> throw Missing(name),
}
}
throw stops the function and hands the error up.
Throwing is an effect, Throws<Missing>, so it
shows up in the signature next to any other effects.
Errors travel up
If you call a function that throws and don't catch the error, it passes through you, and you have to declare it too. A function can throw more than one kind of error:
types
TooPoor = TooPoor(Int)
functions
withdraw(name: String, amount: Int) -> Int / {Throws<Missing>, Throws<TooPoor>} {
let current = balance(name)
if current < amount { throw TooPoor(current) } else { current - amount }
}
withdraw throws TooPoor itself, and lets
Missing from balance pass through.
Catching errors
try runs a block. If something inside it throws, the
matching catch arm runs instead. The arms are patterns,
just like in match:
try_withdraw(name: String, amount: Int) -> String {
try {
"#{name} has #{withdraw(name, amount)} left"
} catch {
Missing(n) -> "no account for #{n}",
TooPoor(left) -> "#{name} only has #{left}",
}
}
Look at the signature: try_withdraw is pure. Both
errors are handled inside it, so its callers never see them, and the
type says so.
Tip: try is an expression,
like if and match. Both the block and
every catch arm produce a value of the same type.
Uncaught errors
An error that reaches the top of main without being
caught is reported, and the program exits with code 1:
$ polar run uncaught.px
uncaught error: Missing("bob")
Assertions
Std.Assert has assert, equal,
not_equal and fail. Each throws a
Failed error when its check fails. They are what
polar test tests are made
of, and handy for quick checks in examples:
uses
Std.Assert
functions
main() {
Assert.assert(1 + 1 == 2)
}
Throws or Result?
Polar gives you two ways to report failure. Here's how to choose:
-
Use
Resultwhen the caller is expected to deal with the failure right away, like parsing user input. It's a value you pass around and match on. See Result. -
Use
throwwhen the failure should skip over several layers of code to wherever it's handled, without every function in between checking for it.
All together
module Accounts
uses
Std.Assert
types
Missing = Missing(String)
TooPoor = TooPoor(Int)
functions
balance(name: String) -> Int / {Throws<Missing>} {
match name {
"ann" -> 120,
"bob" -> 15,
_ -> throw Missing(name),
}
}
withdraw(name: String, amount: Int) -> Int / {Throws<Missing>, Throws<TooPoor>} {
let current = balance(name)
if current < amount { throw TooPoor(current) } else { current - amount }
}
try_withdraw(name: String, amount: Int) -> String {
try {
"#{name} has #{withdraw(name, amount)} left"
} catch {
Missing(n) -> "no account for #{n}",
TooPoor(left) -> "#{name} only has #{left}",
}
}
main() {
Log.info(try_withdraw("ann", 50))
Log.info(try_withdraw("bob", 50))
Log.info(try_withdraw("cat", 50))
Assert.assert(try_withdraw("ann", 20) == "ann has 100 left")
Assert.assert(1 + 1 == 3)
}
exports
main
$ polar run accounts.px
ann has 70 left
bob only has 15
no account for cat
uncaught error: Failed("false")
The first assertion passes. The second fails, and since nothing catches it, the program stops with exit code 1.
Effects tell Polar what a function does. Next, we'll see how Polar uses that to decide where a function can run: Hosts.