ch25.md 6.6 KB

Chapter 25. Exception handling

Nim offers an elaborate, semi-structured mechanism for handling runtime errors called exception handling. Exception handling consists of the try, raise statements, an exception type hierarchy and the .raises annotation system. 25.1. Try statement Example: from std/strutils import parseInt var

f: File

if open(f, "numbers.txt"):

try: 1
  var a = readLine(f) 2
  var b = readLine(f)
  echo "sum: " & $(parseInt(a) + parseInt(b)) 3
except OverflowDefect: 4
  echo "overflow!"
except ValueError, IOError: 5
  echo "catch multiple exceptions!"
except: 6
  echo "Unknown exception!"
finally: 7
  close(f)

1 A try statement. 2 Read the first two lines of a text file. 3 Try to parse them as integers and to sum them. 4 On an OverflowDefect output "overflow!". 5 More than one exception type can be listed in except.

                                                                       165

6 The empty except covers every other exception type. It is comparable to

 an else in an if statement.

7 Regardless of whether there was any exception raised or not the finally

 section is executed. In this case it ensures we always close the file f.

The statements after the try are executed in sequential order unless an exception e is raised. If the exception type of e matches any listed in an except clause, the corresponding statements are executed. The statements following the except clauses are called exception handlers. The empty except clause is executed if there is an exception that is not listed otherwise. If there is a finally clause, it is always executed after the exception handlers. The exception is consumed in an exception handler. However, an exception handler may raise another exception. If the exception is not handled, it is propagated through the call stack. If an exception occurs the "rest" of a routine which is the code that is not within finally or except clauses is not executed. 25.2. Try expression Try can also be used as an expression; the type of the try branch then needs to fit the types of except branches, but the type of the finally branch always has to be void: from std/strutils import parseInt let x = try: parseInt("133a")

       except: -1
       finally: echo "hi"

To prevent confusing code there is a parsing limitation; if the try follows a ( it has to be written as a one liner: from std/strutils import parseInt let x = (try: parseInt("133a") except: -1) 166 25.3. Except clauses Within an except clause it is possible to access the current exception using the syntax ExceptionType as e: try:

# ...

except IOError as e:

echo "I/O error: " & e.msg

Alternatively, it is possible to use system.getCurrentException to retrieve the exception that was raised: try:

# ...

except IOError:

let e = getCurrentException()

Note that getCurrentException always returns a ref Exception type. If a variable of the proper type is needed (in the example above, IOError), one must convert it explicitly: try:

# ...

except IOError:

let e = (ref IOError)(getCurrentException())
# "e" is now of the proper type

However, this is rarely needed. The most common case is to extract an error message from e, and for such situations, it is enough to use system.getCurrentExceptionMsg: try:

# ...

except:

echo getCurrentExceptionMsg()

25.4. Defer statement A defer statement can be used instead of a try finally statement if the try statement lacks any except clauses. In other words, defer can be used to

                                                                       167

ensure resource cleanups (even in case of an error) but not for explicit error handling. Any statements following the defer in the current block will be considered to be in an implicit try block. For example: proc main =

var f = open("numbers.txt", fmWrite)
defer: close(f)
f.write "abc"
f.write "def"

Is rewritten to: proc main =

var f = open("numbers.txt")
try:
  f.write "abc"
  f.write "def"
finally:
  close(f)

When defer is at the outermost scope of a template/macro, its scope extends to the block where the template is called from: template safeOpenDefer(f, path) =

var f = open(path, fmWrite)
defer: close(f)

template safeOpenFinally(f, path, body) =

var f = open(path, fmWrite)
try: body # without `defer`, `body` must be specified as parameter
finally: close(f)

block:

safeOpenDefer(f, "/tmp/z01.txt")
f.write "abc"

block:

safeOpenFinally(f, "/tmp/z01.txt"):
  f.write "abc" # adds a lexical scope

block:

var f = open("/tmp/z01.txt", fmWrite)
try:
  f.write "abc" # adds a lexical scope
finally: close(f)

168 Top-level defer statements are not supported since it’s unclear what such a statement should refer to.


         defer can make the control flow obscure, one should prefer try
         finally or destructors.

25.5. Exception hierarchy Exception types form a hierarchy via inheritance. Most of the hierarchy is defined in the system module. Every exception ultimately inherits from system.Exception. Exceptions that indicate a runtime error that can be caught inherit from system.CatchableError (which is a subtype of Exception). Exceptions that indicate programming bugs inherit from system.Defect (which is a subtype of Exception) and are strictly speaking not catchable as they can also be mapped to an operation that terminates the whole process. 25.6. Raise statement A raise statement is used to signal that an error occurred: raise newException(IOError, "IO failed") Execution of a raise statement starts an unwinding process: The control flow continues at a suitable innermost exception handler. That means for raise e a handler like except typeof(e) or an except without a type guard. If no such handler exists, the program is terminated. A raise statement without an explicit exception object means that the current exception is re-raised. The ReraiseDefect exception is raised if there is no exception to re-raise. It follows that the raise statement always raises an exception. Apart from a raise statement there are other language constructs that can raise an exception: • Array indexing: It is an error if the index is out of bounds.

                                                                         169

• Memory allocation: There is an inherent danger of running out of

memory.

• Integer arithmetic: An operation like x + 1 can produce an overflow

error.

• Type conversion: If x is not of the dynamic type T then T(x) produces an

error.