Exception Handling
Exception handling is the process of responding to and recovering from error conditions in your program. Atem provides first-class support for throwing, catching, propagating, and manipulating recoverable exceptions at runtime.
Defining and Throwing Exceptions
Exceptions are represented by values which type conforms the Exception
Concept. The concept Exception
indicates that a type can be used for exception handling.
Unlike other languages that use classes to represent exceptions, exceptions in Atem are often appear in the form of enumerations. Consider the FilesystemException
example:
FilesystemException: enum implement Exception = {
InvalidPathException(path: String),
FileNotExistException(path: fs.Path),
PermissionDeniedException,
};
A instance of exception type could be thrown by a throw
statement:
throw FilesystemException.FileNotExistException.init(path = .init("/etc/nothing"));
Handling Exceptions
Propagating Exceptions
If your code isn't responsible for handling the exception, you can simply let the exceptions be propagated by throwing functions. Throwing functions propagate exceptions that are thrown inside the function to the caller. To make a function be able to propagate exceptions, mark the function with throws
specifier.
canThrow: throws func = {};
cannotThrow: throws func = {};
In the example below, we implement a copyFile
function which may throw exceptions:
FilesystemException: enum implement Exception = {
InvalidPathException(path: String),
FileNotExistException(path: fs.Path),
PermissionDeniedException,
};
copyFile:
(sourcePath src: String, destinationPath dst: String) -> ()
throws func = {
src_path := fs.Path.init(pathStr = src) ??
throw FilesystemException.InvalidPathException.init(path = src);
dst_path := fs.Path.init(pathStr = dst) ??
throw FilesystemException.InvalidPathException.init(path = dst);
if(not src_path.exist) {
throw FilesystemException.FileNotExistException.init(path = src_path);
}
if(_low_level_copy(src_path, dst_path)) {
throw FilesystemException.PermissionDeniedException.init();
}
};
We can now use the copyFile
function with try
expression which indicates a operation may fail:
copySomething: throws func = { //the function must be marked with throws
try copyFile(sourcePath = "/etc/1", destinationPath = "/etc/2"); //use try expression
};
Catching Exceptions
If you don't want to let the exception to continue propagating up, you can catch exceptions to handle them:
copySomething: func = { //doesn't need for throws specifier because we handle the exception
try copyFile(sourcePath = "/etc/1", destinationPath = "/etc/2")
catch FilesystemException.InvalidPathException(let path) {
println("The path path.to_string()$ is invalid");
}
catch FilesystemException.FileNotExistException(let path) {
println("The file path.to_string()$ doesn't exist");
}
catch FilesystemException.PermissionDeniedException {
println("Doesn't have enough permission to copy the file");
}
};
You can also pack multiple try
expression in a do-catch
block:
copySomething: func = { //doesn't need for throws specifier because we handle the exception
do {
try copyFile(sourcePath = "/etc/1", destinationPath = "/etc/2");
try copyFile(sourcePath = "/etc/3", destinationPath = "/etc/4");
try copyFile(sourcePath = "/etc/5", destinationPath = "/etc/6");
try copyFile(sourcePath = "/etc/7", destinationPath = "/etc/8");
}
catch FilesystemException.InvalidPathException(let path) {
println("The path path.to_string()$ is invalid");
}
catch FilesystemException.FileNotExistException(let path) {
println("The file path.to_string()$ doesn't exist");
}
catch FilesystemException.PermissionDeniedException {
println("Doesn't have enough permission to copy the file");
}
};
The catch
clauses don't need to catch every possible exceptions. If none of the catch
clauses could handle the exception appropriately, the exception will be propagate up again:
copySomething: func = { //doesn't need for throws specifier because we handle the exception
do {
try copyFile(sourcePath = "/etc/1", destinationPath = "/etc/2");
try copyFile(sourcePath = "/etc/3", destinationPath = "/etc/4");
try copyFile(sourcePath = "/etc/5", destinationPath = "/etc/6");
try copyFile(sourcePath = "/etc/7", destinationPath = "/etc/8");
}
catch FilesystemException.InvalidPathException(let path) {
println("The path path.to_string()$ is invalid");
}
catch FilesystemException.FileNotExistException(let path) {
println("The file path.to_string()$ doesn't exist");
}
};
main: func = {
try copySomething() catch FilesystemException.PermissionDeniedException {
println("Doesn't have enough permission to copy the file");
};
};
Catching Exceptions with Optional Value
You can use try?
expression to convert the result of a throwing operation into a optional. If an exception is thrown when executing the try?
expression, the optional will be null
:
someThrowingFunction () -> Int32 throws func = {...};
opt := try? someThrowingFunction();
comptime assert(^opt.type == ?Int32);
println("(opt ?? 0)$");
Catching Exceptions with Variant Value
When an exception is converted to an optional, we lose original exception information. Lacking additional information provided by exception would sometimes be annoying especially when we are debugging the program. To preserve the exception instance, we can also use try??
expression to convert exception into an variant which may contains result of the expression or the exception instance.
someThrowingFunction () -> Int32 throws func = {...};
var := try?? someThrowingFunction();
comptime assert(^var.type == Int32 | some Exception);
if(var is Int32) {
println("(var as Int32)$");
} else var match {
:SomeExceptionType(let msg) = {
println(msg);
}
}
Disabling Exception Propagation
When you are sure a throwing operation won't throw exception, you can use try!
expression to guarantee there is no exception throws in the expression evaluation. If an exception is thrown in a try!
expression, you will get a runtime crash.
someThrowingFunction () -> Int32 throws func = {...};
bar: func = { //no need for throws specifier
foo := try! someThrowingFunction();
};
Using Scope Statement to Perform Cleanup after Exception Handling
We often need to perform some cleanup actions after exceptions being handled. Atem provides three types of defer
statement to do so:
defer exit
will be executed when current scope exitsdefer success
will be executed when current scope success exits with no exception occurreddefer fail
will be executed when current scope exits with exceptions
allocateBufferAndReadFile: () -> Byte.& func = {
buffer := try allocator.allocate(size = 32);
file := try ReadFile();
defer fail {
allocator.deallocate(bufferHandle = buffer);
file.closeFile();
};
buffer.write(file.getFileStream());
return buffer.getPointer();
};