Exception tracking

Nim supports exception tracking. The raises pragma can be used to explicitly define which exceptions a proc/iterator/method/converter is allowed to raise. The compiler verifies this:

  1. proc p(what: bool) {.raises: [IOError, OSError].} =
  2. if what: raise newException(IOError, "IO")
  3. else: raise newException(OSError, "OS")

An empty raises list (raises: []) means that no exception may be raised:

  1. proc p(): bool {.raises: [].} =
  2. try:
  3. unsafeCall()
  4. result = true
  5. except:
  6. result = false

A raises list can also be attached to a proc type. This affects type compatibility:

  1. type
  2. Callback = proc (s: string) {.raises: [IOError].}
  3. var
  4. c: Callback
  5. proc p(x: string) =
  6. raise newException(OSError, "OS")
  7. c = p # type error

For a routine p the compiler uses inference rules to determine the set of possibly raised exceptions; the algorithm operates on p’s call graph:

  1. Every indirect call via some proc type T is assumed to raise system.Exception (the base type of the exception hierarchy) and thus any exception unless T has an explicit raises list. However if the call is of the form f(…) where f is a parameter of the currently analysed routine it is ignored. The call is optimistically assumed to have no effect. Rule 2 compensates for this case.
  2. Every expression of some proc type within a call that is not a call itself (and not nil) is assumed to be called indirectly somehow and thus its raises list is added to p’s raises list.
  3. Every call to a proc q which has an unknown body (due to a forward declaration or an importc pragma) is assumed to raise system.Exception unless q has an explicit raises list.
  4. Every call to a method m is assumed to raise system.Exception unless m has an explicit raises list.
  5. For every other call the analysis can determine an exact raises list.
  6. For determining a raises list, the raise and try statements of p are taken into consideration.

Rules 1-2 ensure the following works:

  1. proc noRaise(x: proc()) {.raises: [].} =
  2. # unknown call that might raise anything, but valid:
  3. x()
  4. proc doRaise() {.raises: [IOError].} =
  5. raise newException(IOError, "IO")
  6. proc use() {.raises: [].} =
  7. # doesn't compile! Can raise IOError!
  8. noRaise(doRaise)

So in many cases a callback does not cause the compiler to be overly conservative in its effect analysis.

Exceptions inheriting from system.Defect are not tracked with the .raises: [] exception tracking mechanism. This is more consistent with the built-in operations. The following code is valid::

  1. proc mydiv(a, b): int {.raises: [].} =
  2. a div b # can raise an DivByZeroDefect

And so is::

  1. proc mydiv(a, b): int {.raises: [].} =
  2. if b == 0: raise newException(DivByZeroDefect, "division by zero")
  3. else: result = a div b

The reason for this is that DivByZeroDefect inherits from Defect and with --panics:on Defects become unrecoverable errors. (Since version 1.4 of the language.)