Throwing and Handling Exceptions

The previous section described how to customize exceptions. This section shows how to throw and handle exceptions.

  • An exception type is a class type. Therefore, you create an exception instance object just like you would do that with any other class. For example, the expression FatherException() creates an exception of the FatherException type.
  • Cangjie provides the throw expression for throwing exceptions. It begins with the throw keyword, and the type of the expression that follows must be a subtype of Exception (you cannot manually throw an Error even though it is also an exception). For example, an arithmetic operation exception gets thrown when throw ArithmeticException("I am an Exception!") is executed.
  • Exceptions thrown by throw expressions need to be captured and handled. If an exception is not handled by the program, the system passes control to the default exception handler.

Exception handling is implemented by try expressions, which can be classified into the following types:

  • Common try expressions that do not involve automatic resource management
  • try-with-resources expressions used for automatic resource management

Common try Expressions

A common try expression includes three parts: a try block, zero or more catch blocks, and an optional finally block.

  • A try block starts with the keyword try followed by a regular code block. The block, enclosed in a pair of curly braces { }, defines a new local scope and can contain any expressions or declarations. Any exceptions thrown in the try block are captured and then an attempt is made to handle them by one of the catch blocks that follow, if any. If there are no catch blocks, or those that are present do not catch exceptions of the given type, the exception is re-thrown after the finally block gets executed.

  • A common try expression can contain zero or more catch blocks. There must be a finally block when there is no catch block. Each catch block starts with the keyword catch, followed by a catchPattern and a block. The catchPattern matches the exceptions to be captured through pattern matching. Once the exception matches, it gets handled by the block following the matching pattern, and other catch blocks, if any, are ignored. When all the exception types that a catch block can catch can be caught by a catch block defined before it, a warning indicating that a catch block is unreachable is reported.

  • A finally block starts with the keyword finally that is followed by a block. In principle, a finally block is mainly used to perform cleanup tasks, such as releasing resources. Ensure that no exception is thrown in the finally block. The content of a finally block is executed regardless of whether an exception occurs (that is, whether an exception was thrown in the try block or not). If the exception was not caught and handled, it gets re-thrown after the finally block is executed. If a try expression contains at least one catch block, the finally block is optional. Otherwise, it must contain a finally block.

The scopes of the try block and each of the catch blocks are independent of each other.

The following is a simple example of an expression with only try and catch blocks:

main() {
    try {
        throw NegativeArraySizeException("I am an Exception!")
    } catch (e: NegativeArraySizeException) {
        println(e)
        println("NegativeArraySizeException is caught!")
    }
    println("This will also be printed!")
}

The execution result is as follows:

NegativeArraySizeException: I am an Exception!
NegativeArraySizeException is caught!
This will also be printed!

The scope level of the variable introduced by the catchPattern is the same as that of the variable in the block following that catch. If the same name is introduced in the catch block, a redefinition error is triggered. The following is an example:

main() {
    try {
        throw NegativeArraySizeException("I am an Exception!")
    } catch (e: NegativeArraySizeException) {
        println(e)
        let e = 0 // Error, redefinition
        println(e)
        println("NegativeArraySizeException is caught!")
    }
    println("This will also be printed!")
}

The following is a simple example of a try expression with a finally block:

main() {
    try {
        throw NegativeArraySizeException("NegativeArraySizeException")
    } catch (e: NegativeArraySizeException) {
        println("Exception info: ${e}.")
    } finally {
        println("The finally block is executed.")
    }
}

The execution result is as follows:

Exception info: NegativeArraySizeException: NegativeArraySizeException.
The finally block is executed.

The try expression can be used wherever an expression is allowed. The method of determining the type of the try expression is similar to that of determining the type of other multi-branch expressions such as if and match expressions. If the result of the try expression is used, its type is the minimum common parent type of the types of all branches except the finally branch. If there is no such common paretn type, a compilation error is reported.

For example, in the following code, the type of the try expression and variable x is the minimum common parent type D of E and D. C() in the finally branch is not involved in the calculation of the common parent type. If it were involved, the minimum common parent type would have changed to C.

open class C { }
open class D <: C { }
class E <: D { }
main () {
    let x = try {
        E()
    } catch (e: Exception) {
        D()
    } finally {
        C()
    }
    0
}

When the value of the try expression is not used, its type is Unit, and the types of its branches are not required to have a minimum common parent type.

try-with-resources Expressions

try-with-resources expressions are used to automatically release non-memory resources. Different from common try expressions, the catch and finally blocks in a try-with-resources expression are both optional. One or more ResourceSpecification can be inserted before the block following the try keyword to apply to a series of resources. ResourceSpecification does not affect the type of the entire try expression. Resources refer to objects at the language level. Therefore, ResourceSpecification is used to instantiate a series of objects. Separate multiple instances with commas ,.

The following is an example of a try-with-resources expression:

class R <: Resource {
    public func isClosed(): Bool {
        true
    }
    public func close(): Unit {
        print("R is closed")
    }
}

main() {
    try (r = R()) {
        println("Get the resource")
    }
}

The output is as follows:

Get the resource

The variables introduced between the try keyword and the following block have the same scope as the variables defined in that block. If an entity with the same name is defined again in the block, a redefinition error is triggered.

class R <: Resource {
    public func isClosed(): Bool {
        true
    }
    public func close(): Unit {
        print("R is closed")
    }
}

main() {
    try (r = R()) {
        println("Get the resource")
        let r = 0 // Error, redefinition
        println(r)
    }
}

The type of a variable introduced by a ResourceSpecification in a try-with-resources expression must implement the Resource interface and ensure that the isClosed function does not throw exceptions.

interface Resource {
    func isClosed(): Bool
    func close(): Unit
}

It should be noted that try-with-resources expressions do not need to contain any catch block or finally block, and you are advised not to manually release resources. No matter whether an exception occurs during the execution of a try block, all applicable resources are automatically released, and all exceptions generated during the execution are thrown. However, if you need to explicitly capture and handle exceptions that may be thrown in the try block or during resource application and release, you can still include catch and finally blocks in a try-with-resources expression.

class R <: Resource {
    public func isClosed(): Bool {
        true
    }
    public func close(): Unit {
        print("R is closed")
    }
}

main() {
    try (r = R()) {
        println("Get the resource")
    } catch (e: Exception) {
        println("Exception happened when executing the try-with-resources expression")
    } finally {
        println("End of the try-with-resources expression")
    }
}

The output is as follows:

Get the resource
End of the try-with-resources expression

try-with-resources expressions are of the Unit type.

Advanced Operations of CatchPattern

In most cases, if you only want to capture exceptions of a certain type and its subtypes, you can use the type pattern of CatchPattern. However, if you need to process all exceptions in a unified manner (for example, errors are reported when exceptions occur), you can use the wildcard pattern of CatchPattern.

The type pattern has two syntax formats:

  • Identifier: ExceptionClass. This format can be used to capture exceptions of the ExceptionClass type and its subclasses, convert the captured exception instance to ExceptionClass, and bind it to the variable defined by Identifier. Then, the captured exception instance can be accessed through the variables defined by Identifier in the catch block.
  • Identifier: ExceptionClass_1 | ExceptionClass_2 | ... | ExceptionClass_n. This format can be used to combine multiple exception classes through the connector |. The connector | indicates an "or" relationship. This way, you can capture exceptions of the ExceptionClass_1 type and its subclasses, exceptions of the ExceptionClass_2 type and its subclasses, and so on, or exceptions of the ExceptionClass_n type and its subclasses (assuming that n is greater than 1) in a single catch block. When the type of the exception to be captured belongs to any type or its subtype in the "or" relationship, the exception is captured. However, the type of the captured exception cannot be statically determined in this case. Therefore, the captured exception is converted to the minimum common parent class of all types connected by |, and the exception instance is bound to the variable defined by Identifier. Therefore, in this mode, the variable defined by Identifier can only be used in the catch block to access the member variables and member functions in the minimum common parent class of ExceptionClass_i (1 <= i <= n).

You can also use wildcards to replace Identifier in the type pattern. The only difference is that wildcards do not perform binding operations.

The following is an example:

main(): Int64 {
    try {
        throw IllegalArgumentException("This is an Exception!")
    } catch (e: OverflowException) {
        println(e.message)
        println("OverflowException is caught!")
    } catch (e: IllegalArgumentException | NegativeArraySizeException) {
        println(e.message)
        println("IllegalArgumentException or NegativeArraySizeException is caught!")
    } finally {
        println("finally is executed!")
    }
    return 0
}

Execution result:

This is an Exception!
IllegalArgumentException or NegativeArraySizeException is caught!
finally is executed!

The following is an example of "the type of the caught exception is the smallest common parent class of all types connected by |":

open class Father <: Exception {
    var father: Int32 = 0
}

class ChildOne <: Father {
    var childOne: Int32 = 1
}

class ChildTwo <: Father {
    var childTwo: Int32 = 2
}

main() {
    try {
        throw ChildOne()
    } catch (e: ChildTwo | ChildOne) {
        println("ChildTwo or ChildOne?")
    }
}

Execution result:

ChildTwo or ChildOne?

The syntax of wildcard pattern is _, which can capture any type of exception thrown in the try block of the same level. It is equivalent to e: Exception in the type pattern, that is, capture the exception defined by an Exception subclass, but it does not bind the exception to any variable. The following is an example:

// Catch with wildcardPattern.
try {
    throw OverflowException()
} catch (_) {
    println("catch an exception!")
}