OxL Reference

Step 5. Errors

Why#

OxL has no exceptions. An exception is an invisible way out of a function: looking at a signature you cannot know what it may "throw". Instead an error is an ordinary value and the possible errors are listed in the result type: -> User | NotFound | Timeout. A type marked @error cannot be ignored: if the result of a function that may return an error is simply discarded, the compiler stops the build.

Separately there are panics: they mean a bug (division by zero, out-of-bounds access, a broken contract) and end the current task; they are not caught and handled like an ordinary error (§6.6).

Example#

oxl✓ checked in CI
@error data NotFound { id: i64 }  // @error marks a type as an error
@error data Invalid { why: str }

fn find(id: i64) -> str | NotFound {  // the result is str or NotFound
  if id == 1 { return "ann" }
  fail NotFound{ id }  // fail returns the error
}

fn parseId(s: str) -> i64 | Invalid {
  match s.parseInt() {
    i64 n => n
    ParseError => fail Invalid{ why: "not a number: {s}" }
  }
}

fn lookup(s: str) -> str | NotFound | Invalid {  // the signature lists every error
  let id = parseId(s)?  // ? passes the error to the caller
  find(id)?
}

fn main() io {
  for s in ["1", "2", "x"] {
    match lookup(s) {  // match takes the result apart by type
      str name => print("found {name}")
      NotFound e => print("no user {e.id}")
      Invalid e => print("bad input: {e.why}")
    }
  }
  let r = await find(2)  // a chain: -> is the next step, !> is a handler
    -> it.len()
    !> NotFound => 0
  print("{r}")
}
Output
found ann
no user 2
bad input: not a number: x
0

Walkthrough#

  • @error data NotFound { id: i64 } - an ordinary record marked as an error. Errors may be an enum as well.
  • -> str | NotFound - "a string or NotFound". fail NotFound{ id } returns the error from the function; the error type must be part of the signature (otherwise - fail-type).
  • parseId(s)? - the ? operator: if the result is an error, the function returns it at once; otherwise the value comes out. The errors of both called functions are therefore listed in the signature of lookup.
  • match takes the result apart by type. If you skip an error you get non-exhaustive.
  • -> is a chain: the next step receives the result as it. !> NotFound => 0 is a handler: the error is replaced by a value and disappears from the result type. Chains are especially useful for network calls and are described in §8.4.
  • If main returns errors (fn main() -> () | Usage | IoError io), an unhandled error ends the process with exit code 1.

A few more ways to work with a result:

FormMeaning
r?return the error from the current function, otherwise extract the value
r!extract the value; on an error - panic (fine in main, tests, scripts)
r is NotFounda type test, narrows r in the if branch
_ = rexplicitly discard the result (rarely and deliberately)
x ?? da default value for T?

Common mistake: an ignored result#

oxl✓ checked in CI
@error data Declined { reason: str }

fn charge(sum: i64) -> i64 | Declined => sum

fn main() {
  charge(10)  // a result carrying an @error cannot be ignored
}
error
unhandled-error

This is the most frequent diagnostic of a beginner: unhandled-error. Fix it in one of these ways: charge(10)? (pass it up), match, charge(10)! (if the error is impossible here), or _ = charge(10) (discard deliberately).

Remember#

  • Errors are values in the result type, not exceptions.
  • ? passes on, match handles, !> replaces in a chain.
  • The network adds NetError to the result of every rpc (§6.5) - it is an ordinary error too, visible in the signature.

Next: collections and iterators.

See also

OxL 1.0.0-beta · Documentation version 1.0.0-beta© OxwaldLink · generated from the Standard, the std sources and the guides