aqua icon indicating copy to clipboard operation
aqua copied to clipboard

Improve errors handling

Open alari opened this issue 4 years ago • 2 comments

In Aqua, errors are handled with AIR's xor instruction, expressed with if, try, otherwise keywords.

The problem is that it's hard to implement the fail-fast approach: if the error is handled by a xor, it is not visible in the code below. Here Aqua follows the underlying AIR semantics.

This leads to weird code structure:

result: *string
if x == 3:

  y <- f(x)
  if y:
   ...
   result <<- "okay"

  else:
    co logError("not y")
else:
  co logError("not 3")

<- result!

Aqua should implement monadic flow instead.

alari avatar Sep 06 '21 08:09 alari

With the new (fail) instruction https://github.com/fluencelabs/aquavm/issues/195 we can do monadic-style errors handling.

alari avatar Dec 16 '21 12:12 alari

Aqua Error Handling Proposal

1. Value -> Error

Service calls may fail. For programmatic error, developer needs to have an explicit error value, e.g.:

func foo() -> ?string, ?Error: ...

To raise an error from such a structure, ? operator can be used:

try:
  x <- foo()?
catch e:
  -- e is value of the last argument, which must be an optional error

Actually, it should be compiled to smth like

try:
  x, e <- foo()?
  join e
  if e != []:
    -- pseudocode: fail does not exist in Aqua
    -- fails this code block on the call site, with no topologic moves
    fail e
catch e:
  -- catch the error, handle

2. Implicit error catching

Let's assume we have a function:

func bar() -> ?string, ?Error:
  a <- foo()
  b <- foo()
  <- a, ...

Using ? at the end of function calls should make the error caught by the function:

func bar() -> ?string, ?Error:
  a <- foo()?
  b <- foo()? -- executed iff `a` yields with no errors
  <- a -- no need to catch, last return value is added implicitly

3. Race of errors, values

Should go to another issue. But as it's expected to play well with error handling, let's introduce here.


func tryOnPeer(peerId: string) -> ?string, ?Error: ...

func getOrFailFastest(p1: string, p2: string) -> ?string, ?Error:
  <- tryOnPeer(p1)? | tryOnPeer(p2)?

| yields the fastest result, be it error or not.

This is especially useful for timeouts. Can be extended to |[x, y, z] builder for an optional first value of any set of arguments, and even |Type to show non-yielded values (that needs join to be used) explicitely.

Questions: for par?

In some cases error management is not that trivial. Consider the following example:


ys: *string

for x <- foo()? par:
  on x.peer_id:
    ys <- foo2()?

<- ys

How should foo2()? be handled? When this snippet yields and when it doesn't?

Often we wait for a threshold:

  • Either length of ys should be > 2/3,
  • Or number of occured errors should be >1/3.

But does it always hold? And 2/3, 1/3 of what, of foo()? size?

for par, and maybe other pars need an explicit error/success state declaration.

Questions: error/either structures?

Services often return structures to show success or error state of the function call. Yet it does not work as Aqua's multi-return feature.

One way to handle it is to unwrap the structure on call site:


data Result:
   is_ok: bool
   success: ?string
   error: ?string

service My("my"):
   call: -> Result

func foo() -> ?string, ?Error:
  x <- My.call()? -- here we raise if Result contains the error
  <- ["ok"] 

Another way is to lift on declaration site:


service My("my"):
  call: -?> Result -- actually returns ?string, ?Error

Question: use ! instead of ?

Error-enforcement operator does not seem like a question, it's more like "do or fail!".

But ! is already used to force access to 0's element of a collection, especially useful for optional values.

alari avatar Apr 11 '22 13:04 alari