Workpad
October 21st, 2024

Try-Catch In UCL - Some Notes

UCL

Stared working on a try command to UCL, which can be used to trap errors that occur within a block. This is very much inspired by try-blocks in Java and Python, where the main block will run, and if any error occurs, it will fall through to the catch block:

try {
  echo "Something bad can happen here"
} catch {
  echo "It's all right. I'll run next"
}

This is all I've got working at the moment, but I want to quickly write some notes on how I'd like this to work, lest I forget it later.

First, much like everything in UCL, these blocks should return a value. So it should be possible to do something like this:

set myResults (try {
  result-of-something-that-can-fail
} catch {
  "My default"
})
--> (result of the thing)

This is kind of like using or in Lua to fallback to a default, just that if the result fails with an error, the default value can be returned from the catch block. In might even be possible to simply this further, and have catch just return a value in cases where an actual block of code is unnecessary:

set myResults (try { result-of-something-that-can-fail } catch "My default")

One other thing to consider is how to represent the error.  Errors are just treated out-of-band at the moment, and are represented as regular Go error types. It might be necessary to add a new error type to UCL, so that it can be passed through to the catch block for logging or switching:

try {
  do-something
} catch { |e|
  echo (cat "The error is " $e)
}

This could also be used as the return value if there is no catch block:

set myResult (try { error "my error" })
--> error: my error

Another idea I have is successive catch blocks, that would cascade one after the other if the one before it fails:

try {
  do-something
} catch {
  this-may-fail-also
} catch {
  echo "Always passes"
}

Unlike JavaScript or Python, I don't think the idea of having catch blocks switching based on the error type would be suitable here. UCL is dynamic in nature, and having this static type checking feels a little wrong here. The catch blocks will only act as isolated blocks of execution, where an error would be caught and handled.

Finally, there's or0, which would run regardless of which try or catch block was executed. I think, unlike the other two blocks, that the return value of a or1 block will always be swallowed. I think this will work as the finally block should mainly be used for clean-up, and it's the result of the or2 or or3 blocks that are more important.

set res (try {
  "try"
} catch {
  "catch"
} finally {  
  "finally"
})
--> "try"

Anyway, this is the idea I have right now.

Update — I just realised that the idea of the last successful try block return an error, rather than letting it climb up the stack defeats the purpose of exceptions. So having something like the following:

try { error "this will fail" }

Should just unroll the stack and not return an error value. Although if there is a need to have an error value returned, then the following should work:

try { error "this will fail" } catch { |err| $err }
--> error: this will fail