Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Appendix G: The Error Hierarchy

Every error in Zuri is an instance of a class, and every one of them inherits from Error. They are declared in ordinary Zuri and go through the same class machinery user code does, which is why subclassing one behaves exactly like subclassing anything else.

Error
├── TypeError
├── ValueError
├── NumericError
├── ArgumentError
├── NotImplementedError
├── RangeError
├── AccessError
├── AssertError
├── PropertyError
├── UndefinedError
└── ModuleNotFoundError

Fields

Every error carries three:

FieldWhat it holds
messagethe text, defaulting to 'An unexpected error has occurred'
typethe class name, as a string
stacktracea list of frames, innermost first
catch {
  raise ValueError('bad input')
} as e {
  echo e.type
  echo e.message
  echo e.stacktrace
}
ValueError
bad input
[/path/to/main.zu:2 -> @.script()]

When Each One Is Raised

ClassRaised when
Errorthe base class; a general failure with nothing more specific to say
TypeErroran operation received the wrong type: an undefined operator signature, a method on nil, an annotated parameter given the wrong thing
ValueErrorthe type was right and the value was not
NumericErroran arithmetic operation failed
ArgumentErrora call passed the wrong number of arguments to a native function
NotImplementedErrora method meant to be overridden was not
RangeErroran index or a bound fell outside what the value allows
AccessErrora permission or access check failed
AssertErroran assert condition was falsy
PropertyErrora member that does not exist was read: a missing dictionary key, an undeclared field, a module member that was not exported
UndefinedErroran undefined global was read
ModuleNotFoundErroran import could not be resolved

Catching

catch catches everything inside its block. To handle one kind and let the rest through, test and re-raise:

catch {
  load_config()
} as e {
  if !instance_of(e, ModuleNotFoundError) {
    raise e
  }

  echo 'no config, using defaults'
}

instance_of() walks the whole chain, so a test against Error matches everything.

A parameter annotated Error accepts any of them, which is the readable way to write a handler:

def report(e: Error) {
  echo '${e.type}: ${e.message}'
}

Subclassing

class HttpError < Error {
  @new(message, status) {
    parent(message)

    self.type = 'HttpError'
    self.status = status
  }
}

Two things make this work well. Call parent(message) so the base constructor sets message and the stack trace is captured. Set self.type so the class name appears in logs and in the uncaught-error banner.

Carry whatever the handler needs. An error class exists precisely so it can hold more than a string; the capstone’s TaskError carries the name of the field that failed validation, and that is what lets an API answer {"error": "...", "field": "title"}.

Uncaught

An error nobody catches prints its type, its message, the source around the failure, and the stack trace, then exits 1:

Unhandled ValueError: bottomed out
  --> /path/to/main.zu:3

  1 | def recurse(n) {
  2 |   if n <= 0 {
> 3 |     raise ValueError("bottomed out")
  4 |   }
  5 |   recurse(n - 1)

Stack trace (most recent call last):
  at recurse() /path/to/main.zu:3
  at recurse() /path/to/main.zu:5
  ... 19 more frames ...
  at recurse() /path/to/main.zu:5

A deep stack is truncated in the middle. The top and the bottom are the parts that tell you anything.

There Is No finally

Code after a catch statement runs whether the block raised or not, because the handler either recovers or re-raises. See Error Handling for the patterns that replace it.