1. Error handling
This section will introduce error handling in the Ymir language. As
with many languages, error are managed by throwing
exceptions. Exception can be recovered thanks to scope guards that
manage the errors in different manners. The keyword throw
is used to
throw an exception when an error occurs in the program. An exception
is a class that inherits the core class core::exception::Exception
.
Exceptions are always recoverable, and must be managed by the user,
who cannot simply ignore them. Ymir does not allow the possibility
to ignore that an exception is thrown in a function, and may cause the
function to exit prematurely. To avoid this possibility, excpetion
must be written in the definition of the function, or managed
directly.
// Exception is defined in a core module, so does not need import
class MyError over Exception {
pub self () {}
}
def main ()
throws &MyError
{
throw MyError::new ();
}
1.1. Assert
We have seen in previous section the assert expression. This simple
expression throws an &AssertError
value when the condition is
not valid. AssertError
is a common exception defined in a core
file (that does not need to be imported).
def main ()
throws &AssertError
{
let i = 11;
assert (i < 10, "i must be lower than 10")
}
1.2. Rethrowing
The error rethrowing is a way of defining that a function could throw
an exception, and that this exception must be taking into account by
the caller functions. It is a system relatively close to error
rethrowing of the Java language, apart that the specific name of the
exception must be written in the possible rethrowed exceptions. That
is to say, it is impossible to write that a function throws a parent
class of the actually thrown exception (e.g. &Exception
, when
the function actually throws &AssertError
). Thanks to that, the
compiler is always able to check the type of the exceptions, and can
force the user to handle them.
In the following example, the function foo
is an unsafe function
that can throw the exception &AssertError
. This exception is
thrown by the function assert
at line 6
, and is not
managed by the function foo
. Because the main
function
calls the foo
function, it is also unsafe, and also throws the
exception &AssertError
. In this example, the program stops,
because of an unhandled exception.
import std::io
def foo (i : i32)
throws &AssertError
{
assert (i < 10, "i is not lower than ten");
println (i);
}
def main ()
throws &AssertError
{
foo (10);
}
Results (in debug mode, -g option):
Unhandled exception
Exception in file "/home/emile/ymir/Runtime/midgard/core/exception.yr", at line 84, in function "core::exception::abort", of type core::exception::AssertError.
╭ Stack trace :
╞═ bt ╕ #1
│ ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #2
│ ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #3 in function core::exception::abort (...)
│ ╘═> /home/emile/ymir/Runtime/midgard/core/exception.yr:84
╞═ bt ╕ #4 in function main::foo (...)
│ ╘═> /home/emile/Documents/test/ymir/main.yr:7
╞═ bt ╕ #5 in function main (...)
│ ╘═> /home/emile/Documents/test/ymir/main.yr:10
╞═ bt ╕ #6
│ ╘═> /lib/libgyruntime.so:??
╞═ bt ╕ #7 in function main
│ ╘═> /home/emile/Documents/test/ymir/main.yr:10
╞═ bt ╕ #8
│ ╘═> /lib/x86_64-linux-gnu/libc.so.6:??
╞═ bt ═ #9 in function _start
╰
Aborted (core dumped)
The compiler does not allow to forget the possibility of a error
throwing, and requires the user to write it down. In the following
example, the function foo
call the function assert
that
could throw an &AssertError
if the test fails. In that case the
function foo
can also throw an error, and that must be written
in the prototype of the function. Otherwise the compiler gives an
error.
import std::io
def foo (i : i32)
{
assert (i < 10, "i is not lower than ten");
println (i);
}
Errors:
Error : the function main::foo might throw an exception of type &(core::exception::AssertError), but that is not declared in its prototype
--> main.yr:(3,5)
3 ┃ def foo (i : i32)
╋ ^^^
┃ Note :
┃ --> main.yr:(5,2)
┃ 5 ┃ assert (i < 10, "i is not lower than ten");
┃ ╋ ^
┗━━━━━┻━
ymir1: fatal error:
compilation terminated.
As previously stated, the name of the exceptions specified in the
prototype function must be the actual name of the exception, not the
name of an ancestor. In the following example, the class
ParentException
and ChildException
are two throwable
class. The function foo
throws an object of type
ChildException
, but the prototype declares that the function
throws a ParentException
object. To avoid losing accuracy, the
Ymir language does not allow that. This is however still possible to
perform this kind of behavior (necessary when the function throw
multiple kind of errors, all deriving from ParentException
for
example), by using a cast
, that we have seen in chapter Cast
and Dynamic
typing. That way,
there is a loss of accuracy, but properly defined and intended by the
user.
class ParentException over Exception {
pub self () {}
}
class ChildException over Exception {
pub self () {}
}
def foo ()
throws &ParentException
{
throw ChildException::new ()
}
Errors:
Error : the function main::foo might throw an exception of type &(main::ChildException), but that is not declared in its prototype
--> main.yr:(9,5)
9 ┃ def foo ()
╋ ^^^
┃ Note :
┃ --> main.yr:(12,5)
┃ 12 ┃ throw ChildException::new ()
┃ ╋ ^^^^^
┗━━━━━┻━
Error : the function main::foo prototype informs about a possible throw of an exception of type &(main::ParentException), but this is not true
--> main.yr:(9,5)
9 ┃ def foo ()
╋ ^^^
┃ Note :
┃ --> main.yr:(10,12)
┃ 10 ┃ throws &ParentException
┃ ╋ ^
┗━━━━━┻━
ymir1: fatal error:
compilation terminated.