Skip to main content

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 exits
  • defer success will be executed when current scope success exits with no exception occurred
  • defer 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();
};