Errors: try, catch, raise
Meta: positioning matters: errors are not for control flow. Open with "Dang uses errors for errors — recoverable failures across boundaries — not for null or expected branches."
Dang uses errors for errors — recoverable failures across boundaries — not for null or expected branches.
Raising
raise "something went wrong"
raise NotFoundError(message: "user gone", resource: "User")
raisetakes aString!or anError!; anything else is a compile error (raise requires a String! or Error!, got Int!)- raising a string wraps it in a built-in
BasicError(implementsError,message= the string) - raising a value implementing
Errorraises it as-is raiseis itself an expression of any type (fresh type var), so it can sit in any branch (e.g. acase/catcharm) without breaking the merged result type
The Error interface
interface Error {
pub message: String!
}
Erroris a real prelude interface;BasicErroris the built-in implementer for string raises- user error types must
implements Error, which forces apub message: String!field (see Interfaces and unions) - additional fields are preserved on the raised value and matchable in
catch(e.g.resource,field,code) Error!is usable like any interface type — as a fn param, in a type pattern, etc.
Catching
try {
validate(name)
} catch {
err => "fallback: " + err.message
}
- whole
try/catchis one expression — assignable, returnable, usable inline (incl. nested / in alet) - the success value of the body passes through unchanged when nothing is raised
- the body and every catch clause must merge to one type, else compile error (
cannot use String! as Int!) - catches errors raised anywhere in the body, including ones propagated out of called functions and runtime errors (e.g.
1 / 0→division by zero)
Type-pattern catches
try { ... } catch {
v: ValidationError => v.field
n: NotFoundError => n.resource
e: Error => e.message # interface pattern = typed catch-all
err => err.message # bare catch-all, err: Error!
}
- catch clauses are the same patterns as
case, but limited to type patterns and a catch-all (no value patterns) - first match wins
- pattern types must implement
Error(validated against theError!operand, like acaseover an interface — see Control flow) - the bare catch-all binds the error as
Error! - the
Errorinterface itself works as a pattern, matching any error; place specific types before it
Propagation
- uncaught errors unwind through enclosing function calls
- a
raisewith no enclosingcatchterminates the program (uncaught error: <message>) returncannot be caught; it's not an error —try { return x } catch {..}still returnsxfrom the function- re-raise inside a catch with
raise err(or a new error); it propagates to the next enclosingcatch
When to raise vs. return null
Meta: a small "when to raise vs. when to return null" table would help here. The rule of thumb: raise when continuing would yield wrong results; return null when absence is normal.
| situation | use |
|---|---|
| absence is a normal, expected outcome | return null (nullable type) |
| caller routinely branches on the result | return null / a result value |
| continuing would produce wrong results | raise |
| failure crosses a boundary (validation, HTTP/GraphQL, contract) | raise |
Common patterns
- input validation
- failed external calls (HTTP/GraphQL)
- contract violations
Anti-patterns
- using
raisefor early exit (usereturn— see Control flow) - using
raiseto signal expected absence (returnnull— see Flow-sensitive typing) - catching
Errorjust to ignore it