Chapter 9: Exception Handling

Exception handling in Myst is really a more complex form of break and next. Exceptions are generally used to immediately halt execution of a function and panic upwards until an appropriate handler is found.

Myst adopts the raise and rescue keyword terminology from its Ruby ancestry. While the term "throw" may sometimes be used interchangeably with "raise", Myst does not implement the throw and catch keywords that Ruby does, as the same behavior can easily be implemented with raise/rescue.

Raise

Raising an exception in Myst is done using the raise keyword. raise expects to be given a value as an argument, but this value can be any value, even nil:

1raise "woops"
2raise 1024 - 512
3raise %Foo{}
4raise nil

When ever a raise is encountered, execution of the current function will immediately stop (after the value has been created), and Myst will backtrack up the callstack until an appropriate handler is found (we'll see what this looks like in a bit).

If no matching handler for the exception is found, the program will exit with a non-zero exit status and a message of the error with the callstack that caused it.

While Myst allows any value to be raised, it can be useful to use common, default exception types to simplify how they are handled.

Rescue

Once an exception has been raised, the only way to recover and continue execution of the program is with a rescue clauses.

rescue clauses are most commonly defined as suffixes for method definitions. The most basic example is a rescue with no parameter:

 1def bar
 2  raise "woops"
 3end
 4
 5def foo
 6  bar
 7  :finished_normally
 8rescue
 9  :rescued
10end
11
12foo #=> :rescued

Notice that the return value of calling foo is :rescued, not :finished_normally. Because bar raised an exception, the language immediately started panicking. This panic was stopped by the rescue on foo, meaning :finished_normally was never encountered in the main body of foo.

rescue clauses may also provide a pattern-matched parameter to check against the exception being raised:

 1def baz
 2  raise "woops"
 3end
 4
 5def bar
 6  baz
 7rescue n : Integer
 8  :rescued_integer
 9end
10
11def foo
12  bar
13rescue e
14  :rescued_anything
15end
16
17foo #=> :rescued_anything

In this case, baz raised the String value "woops". While bar defines a rescue clause, its parameter specifies an Integer value, which does not match the String that has been raised, so panicking continues.

foo's rescue clause simply defines a name for the exception, which will always match, so the exception is caught and the clause is evaluated, returning rescued_anything.

Just like a when chain, multiple rescues can be given on the same method to match against different exceptions in the same place:

 1def foo
 2  raise "bar"
 3rescue Integer
 4  # do something...
 5rescue "bar"
 6  :rescued_bar
 7rescue e
 8  # do something else...
 9end
10
11foo #=> :rescued_bar

The parameter for a rescue clause is just like a normal function parameter. Beyond the type matching shown above, this means that the parameter can be matched against literal values, destructurings, or even value interpolations! Myst's Spec library makes good use of this to define an expect_raises assertion:

1def expect_raises(expected_error, &block)
2  block()
3  raise %AssertionFailure{@name, expected_error, "no error"}
4rescue <expected_error>
5  # If the raised error matches what was expected, the assertion passes.
6rescue received_error
7  raise %AssertionFailure{@name, expected_error, received_error}
8end

Here, expect_raises calls block, then defines two exception handlers. The first dynamically interpolates the expected_error as the parameter of the rescue, which will only succeed if block raises a matching error, meaning the assertion passes.

Otherwise, the second handler matches any other raised exception, and raises a new failure that the received exception did not match what was actually raised, causing the assertion to fail.

Ensure

Sometimes raising an error in a block of code could leave a program in a bad or currupted state. Leaving a file open, not calling a callback function, etc. These are all things that could cause a successful recovery of a program with rescue to actually cause further failure. To help address this, Myst provides ensure.

ensure clauses come at the end of a rescue chain (or on their own), and will always be executed, even while panicking up the callstack during a raise; even when the exception has not been rescued.

Here's a trivial example that shows the semantics of ensure:

 1@did_ensure = false
 2def foo
 3  raise "woops"
 4  :finished
 5rescue Integer
 6  :rescued
 7ensure
 8  @did_ensure = true
 9  :ensured
10end
11
12foo #=> :rescued
13@did_ensure #=> true

So here's an interesting caveat. We clearly hit the ensure clause, because @did_ensure got set to true. But, the return value of foo was :rescued. Why? Because ensure cannot affect the return value of a function.

More than anything, this is for logical simplicity when dealing with no errors. Here's an even simpler example:

1def line_count
2  f = File.open("test.txt")
3  f.lines.size
4ensure
5  f.close
6end

Here, the use of the ensure block is just to guarantee that the file f gets closed properly. But, we want the return value of the function to be the number of lines in the file.

If ensure did change the return value, we'd have to save the line count into a local variable, then remember to add that variable as the return value after f.close in the ensure clause. Even if we used return f.lines.size, the same problem would occur, since ensure will always run after a function completes.

So, for simplicity, ensure is guaranteed to not affect the return value of a function.

Exception handling on non-methods

Beyond exception handling on method definitions, Myst also allows exception handlers to be defined on blocks and anonymous function clauses using the do...end syntax. Since blocks and anonymous functions are semantically equivalent to normal functions, exception handlers work exactly the same way:

 1block_result = [1, 2, 3].each do |e|
 2  raise "woops"
 3  e
 4rescue
 5  :rescued
 6end
 7
 8result #=> :rescued
 9
10func = fn
11  ->(2) do
12    raise "woops"
13  rescue
14    :rescued
15  end
16end
17
18func(2) #=> :rescued

Note that exception handlers are not allowed with the brace-block syntax. The result is too visually jarring and is inconsistent with keyword blocks always terminating with an end keyword.

Get Started

Introduction Values and Variables Basic Operations Flow Control Pattern Matching Functions and Clauses Modules Types and Self Blocks and Anonymous Functions Exception Handling Loading Code