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 theFatherException
type. - Cangjie provides the
throw
expression for throwing exceptions. It begins with thethrow
keyword, and the type of the expression that follows must be a subtype ofException
(you cannot manually throw anError
even though it is also an exception). For example, an arithmetic operation exception gets thrown whenthrow 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 keywordtry
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 thetry
block are captured and then an attempt is made to handle them by one of thecatch
blocks that follow, if any. If there are nocatch
blocks, or those that are present do not catch exceptions of the given type, the exception is re-thrown after thefinally
block gets executed. -
A common
try
expression can contain zero or more catch blocks. There must be afinally
block when there is nocatch
block. Eachcatch
block starts with the keywordcatch
, followed by acatchPattern
and a block. ThecatchPattern
matches the exceptions to be captured through pattern matching. Once the exception matches, it gets handled by the block following the matching pattern, and othercatch
blocks, if any, are ignored. When all the exception types that acatch
block can catch can be caught by acatch
block defined before it, a warning indicating that acatch
block is unreachable is reported. -
A
finally
block starts with the keywordfinally
that is followed by a block. In principle, afinally
block is mainly used to perform cleanup tasks, such as releasing resources. Ensure that no exception is thrown in thefinally
block. The content of afinally
block is executed regardless of whether an exception occurs (that is, whether an exception was thrown in thetry
block or not). If the exception was not caught and handled, it gets re-thrown after thefinally
block is executed. If atry
expression contains at least onecatch
block, thefinally
block is optional. Otherwise, it must contain afinally
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 theExceptionClass
type and its subclasses, convert the captured exception instance toExceptionClass
, and bind it to the variable defined byIdentifier
. Then, the captured exception instance can be accessed through the variables defined byIdentifier
in thecatch
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 theExceptionClass_1
type and its subclasses, exceptions of theExceptionClass_2
type and its subclasses, and so on, or exceptions of theExceptionClass_n
type and its subclasses (assuming thatn
is greater than 1) in a singlecatch
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 byIdentifier
. Therefore, in this mode, the variable defined byIdentifier
can only be used in the catch block to access the member variables and member functions in the minimum common parent class ofExceptionClass_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!")
}