Exceptions
When developing software systems, detecting and handling errors in the program is often quite challenging. To ensure the correctness and robustness of the system, many software systems contain a large amount of code dedicated to error detection and handling. An exception is a special type of error that can be captured and handled by programmers. It is a general term for a series of abnormal behaviors that occur during program execution, such as out-of-range index, divide-by-zero error, calculation overflow, and invalid input.
An exception is not part of the normal functionality of a program. Once an exception occurs, the program must handle the exception immediately. That is, the control right of the program is transferred from the execution of the normal functionality to the exception handling part. Cangjie provides an exception handling mechanism to handle various exceptions that may occur during program running, including the following:
-
try expression: including ordinary try expression and try-with-resources expression.
-
throw expression: consists of the keyword
throw
and a trailing expression whose type must be inherited from theException
orError
class.
The following describes the try and throw expressions.
try Expressions
The try expressions are classified into ordinary try expressions that do not involve automatic resource management and try-with-resources expressions that involve automatic resource management. The syntax of the try expression is defined as follows:
tryExpression
: 'try' block 'finally' block
| 'try' block ('catch' '(' catchPattern ')' block)+ ('finally' block)?
| 'try' '(' ResourceSpecifications ')' block ('catch' '(' catchPattern ')' block)* ('finally' block)?
;
catchPattern
: wildcardPattern
| exceptionTypePattern
;
exceptionTypePattern
: ('_' | identifier) ':' type ('|' type)*
;
ResourceSpecifications
: ResourceSpecification (',' ResourceSpecification)*
;
ResourceSpecification
: identifier (':' classType)? '=' expression
;
The following describes the ordinary try expression and try-with-resources expression.
Ordinary try Expressions
An ordinary try expression (the try expression mentioned in this section refers to an ordinary try expression) consists of three parts: try block, catch block, and finally block.
-
A try block starts with the keyword
try
and is followed by a block enclosed in curly braces that contains any expressions and declarations (defining a new local scope). The block following try can throw an exception, which is caught and handled by the catch block. (If no catch block exists or the exception is not caught, after the finally block is executed, the exception is thrown to the calling function.) -
Zero or more catch block can be contained in a try expression. (The finally block is required when there is no catch block.) Each catch block starts with the keyword
catch
, followed by a(catchPattern)
and a block consisting of expressions and declarations.catchPattern
matches exceptions to be caught using pattern matching. Once a match is found, the exception is handled by the block consisting of expressions and declarations, and all subsequent catch blocks are ignored. When all exception types that can be captured by a catch block can also be captured by another catch block defined before it, a warning indicating that the catch block is unreachable is displayed at that catch block. -
The finally block starts with the keyword
finally
and is followed by a block consisting of expressions and declarations enclosed in curly braces. In principle, the finally block is used to perform "cleanup" tasks, such as releasing resources. Throwing exceptions in the finally block should be avoided. The content in the finally block is executed regardless of whether an exception occurs (that is, whether an exception is thrown in the try block). If the exception is not handled, the exception is thrown after the finally block is executed. In addition, a try expression can contain one finally block or no finally block (in this case, at least one catch block must be present).
The catchPattern
has two patterns:
-
Wildcard pattern ("_") captures any type of exceptions thrown in the try block at the same level. This pattern is equivalent to the
e: Exception
pattern, that is, capturing the exceptions defined by Exception and its subclasses. Example:// Catch with wildcardPattern. let arrayTest: Array<Int64> = Array<Int64>([0, 1, 2]) try { let lastElement = arrayTest[3] } catch (_) { print("catch an exception!") }
-
Type pattern catches exceptions of a specified type (or its subclass). There are two main syntaxes:
identifier : ExceptionClass
: This format can be used to capture exceptions of the ExceptionClass type and its subclasses, convert the captured exception instances to ExceptionClass, and bind the exception instances to the variables defined byidentifier
. Then, you can access the captured exception instances through the variables defined byidentifier
in the catch block.identifier : ExceptionClass_1 | ExceptionClass_2 | ... | ExceptionClass_n
| : | ... | This format uses the|
connector to combine multiple exception classes.|The connector|
indicates the OR relationship.|You can capture exceptions of theExceptionClass_1
type and its subclasses, exceptions of theExceptionClass_2
type and its subclasses, and similarly, exceptions of theExceptionClass_n
type and its subclasses (assuming that n is greater than 1). When the type of the exception to be captured belongs to any class or subclass in the OR relationship, the exception is captured. However, because the type of the caught exception cannot be statically determined, the type of the caught exception is converted to the smallest common superclass of all types connected by|
.|Then, the exception instance is bound to the variable defined byidentifier
. Therefore, in the type pattern, within the catch block, you can only access the member variables and member functions from the minimum common superclass ofExceptionClass_i (1 <= i <= n)
through the variables defined byidentifier
. You can also use wildcards to replaceidentifier
in the type pattern. The only difference is that no binding will occur.
Examples of using type patterns are as follows:
// The first situation. main() { try { throw ArithmeticException() } catch (e: Exception) { // Caught. print("Exception and its subtypes can be caught here") } }
// The second situation. // User defined exceptions. open class Father <: Exception { var father: Int64 = 0 func whatFather() { print("I am Father") } } class ChildOne <: Father { var childOne: Int64 = 1 func whatChildOne() { print("I am ChildOne") } func whatChild() { print("I am method in ChildOne") } } class ChildTwo <: Father { var childTwo: Int64 = 2 func whatChildTwo() { print("I am ChildTwo") } func whatChild() { print("I am method in ChildTwo") } } // Function main. main() { var a = 1 func throwE() { if (a == 1) { ChildOne() } else { ChildTwo() } } try { throwE() } catch (e: ChildOne | ChildTwo) { e.whatFather() // ok: e is an object of Father //e.whatChildOne() // error: e is an object of Father //e.whatChild() // error: e is an object of Father print(e.father) // ok: e is an object of Father //print(e.childOne) // error: e is an object of Father //print(e.childOTwo) // error: e is an object of Father } return 0 }
An example of using the finally block is as follows:
// Catch with exceptionTypePattern.
try {
throw IndexOutOfBoundsException()
} catch (e: ArithmeticException | IndexOutOfBoundsException) {
print("exception info: " + e.toString())
} catch (e: Exception) {
print("neither ArithmeticException nor IndexOutOfBoundsException, exception info: " + e.toString())
} finally {
print("the finally block is executed")
}
Types of try Expressions
Similar to if expressions:
- If the value of the
try
expression is not read or returned, the type of the entiretry
expression is Unit. The try block and catch block do not require a common supertype. Otherwise, see the following rules. - When there is no explicit type requirement in the context, the try block and all catch blocks (if any) are required to have a minimum common supertype. The type of the entire try expression is the minimum common supertype.
- When the context has specific type requirements, the type of the try block and any catch block (if any) must be a subtype of the type required by the context. In this case, the try block and any catch block are not required to have a minimum common supertype.
Note that although the finally block is executed after the try block and catch block, it does not affect the type of the entire try expression. In addition, the type of the finally block is always Unit
(even if the type of the expression in the finally block is not Unit
).
Evaluation Order of try Expressions
Additional rules for executing the try {e1} catch (catchPattern) {e2} finally {e3}
expression are as follows:
-
If the
return e
expression is reached before entering the finally block, the value ofe
is evaluated tov
, and then the finally block is executed immediately. If thebreak
orcontinue
expression is reached before entering the finally block, the finally block is executed immediately.- If the finally block does not contain the return expression, the cached result
v
is returned (or an exception is thrown) after the finally block is processed. That is, even if the finally block assigns a value to the variable referenced ine
, thev
that has already been evaluated is not affected. The following is an example:
func f(): Int64 { var x = 1; try { return x + x; // Return 2. } catch (e: Exception) { // Caught. print("Exception and its subtypes can be caught here") } finally { x = 2; } // The return value is 2 but not 4.
- If
return e2
orthrow e2
is executed in the finally block, the value ofe2
is evaluated to the resultv2
, andv2
is returned or thrown immediately. Ifbreak
orcontinue
is executed in the finally block, the execution of the finally block is terminated and the loop exits immediately. Example:
func f(): Int64 { var x = 1; try { return x + x; // Return 2. } catch (e: Exception) { // Caught. print("Exception and its subtypes can be caught here") } finally { x = 2; return x + x; // Return 4 } // The return value is 4 but not 2. }
- If the finally block does not contain the return expression, the cached result
In a word, the finally block is always executed. If any control transfer expression exists in the finally block, it overrides any control transfer expression encountered before entering the finally block.
The handling of throw
in the try expression is more complex. For details, see the next section.
Exception Handling Logic of try Expressions
Rules for throwing an exception when the try {e1} catch (catchPattern) {e2} finally {e3}
expression is executed are as follows:
-
No exception is thrown during the execution of
e1
(in this case,e2
is not executed):- If no exception is thrown during the execution of
e3
, the entire try expression does not throw an exception. - If the
E3
exception is thrown during the execution ofe3
, the entire try expression throws theE3
exception.
- If no exception is thrown during the execution of
-
If the
E1
exception is thrown during the execution ofe1
and theE3
exception is thrown during the execution ofe3
, the entire try expression throws theE3
exception (regardless of whether theE1
is captured by the catch block). -
E1
is thrown during the execution ofe1
and no exception is thrown during the execution ofe3
:- If
E1
can be captured by the catch block and no exception is thrown during the execution ofe2
, no exception is thrown in the entire try expression. - If
E1
can be captured by the catch block and the exceptionE2
is thrown during the execution ofe2
, the exceptionE2
is thrown in the entire try expression. - If
E1
is not captured by the catch block, the entire try expression throws the exceptionE1
.
- If
try-with-resources Expressions
The try-with-resources expression is used to automatically release non-memory resources. Different from ordinary try expressions, the catch block and finally block in the try-with-resources expression are optional. Between the try keyword and the block following it, one or more ResourceSpecifications can be inserted to request for a series of resources (ResourceSpecifications do not affect the type of the entire try expression). The resource here refers to objects. Therefore, ResourceSpecification is to instantiate a series of objects (with multiple instantiations separated by commas). The following is an example of using the try-with-resources expression:
class MyResource <: Resource {
var flag = false
public func isClosed() { flag }
public func close() { flag = true }
public func hasNextLine() { false }
public func readLine() { "line" }
public func writeLine(_: String) {}
}
main() {
try (input = MyResource(),
output = MyResource()) {
while (input.hasNextLine()) {
let lineString = input.readLine()
output.writeLine(lineString)
}
} catch (e: Exception) {
print("Exception happened when executing the try-with-resources expression")
} finally {
print("end of the try-with-resources expression")
}
}
In the try-with-resources expression, the type of ResourceSpecification
must implement the Resource interface.
interface Resource {
func isClosed(): Bool
func close(): Unit
}
The try-with-resources expression first requests the series of resources required for instantiation in the order they are declared. (In the preceding example, the input object is instantiated before the output object). If a resource fails to be allocated (for example, the output object fails to be instantiated), all resources that have been successfully allocated (for example, the input object) are released. (Any exception thrown during the release is ignored.) In addition, an exception is thrown, indicating that the resource (output object) fails to be allocated.
If all resources are successfully allocated, the block following try
continues to be executed. During block execution, resources are automatically released in the reverse order of resource allocation regardless of whether an exception occurs. (In the preceding example, the output object is released before the input object.) During resource release, if an exception occurs when a resource is released, the release of other resources is not affected. In addition, the exception thrown by the try expression complies with the following principles: (1) If an exception is thrown in the block following try
, the exception thrown during resource release is ignored. (2) If no exception is thrown in the block following try
, the first exception thrown during resource release is thrown (exceptions thrown during subsequent resource release will be ignored).
Note that the try-with-resources expression does not need to contain the catch block and finally block, and you are advised not to manually release resources. During the execution of the try block, all allocated resources are automatically released regardless of whether an exception occurs, 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 allocation and release, you can still include the catch block and finally block in the try-with-resources expression.
try (input = MyResource(),
output = MyResource()) {
while (input.hasNextLine()) {
let lineString = input.readLine()
output.writeLine(lineString)
}
} catch (e: Exception) {
print("Exception happened when executing the try-with-resources expression")
} finally {
print("end of the try-with-resources expression")
}
The preceding try-with-resources expression is equivalent to the following ordinary try expressions:
try {
var freshExc = None<Exception> // A fresh variable that could store any exceptions
let input = MyResource()
try {
var freshExc = None<Exception>
let output = MyResource()
try {
while (input.hasNextLine()) {
let lineString = input.readLine()
output.writeLine(lineString)
}
} catch (e: Exception) {
freshExc = e
} finally {
try {
if (!output.isClosed()) {
output.close()
}
} catch (e: Exception) {
match (freshExc) {
case Some(v) => throw v // Exception raised from the user code will be thrown
case None => throw e
}
}
match (freshExc) {
case Some(v) => throw v
case None => ()
}
}
} catch (e: Exception) {
freshExc = e
} finally {
try {
if (!input.isClosed()) {
input.close()
}
} catch (e: Exception) {
match (freshExc) {
case Some(v) => throw v
case None => throw e
}
}
match (freshExc) {
case Some(v) => throw v
case None => ()
}
}
} catch (e: Exception) {
print("Exception happened when executing the try-with-resources expression")
} finally {
print("end of the try-with-resources expression")
}
If an exception is thrown in the try
block (that is, user code), the exception is recorded in the freshExc
variable and is finally thrown layer by layer. The priority of the exception is higher than that of the exception that may occur during resource release. The type of the try-with-resources expression is Unit
.
throw Expressions
The syntax of the throw expression is defined as follows:
throwExpression
: 'throw' expression
;
A throw expression consists of the keyword throw
and an expression. It is used to throw an exception. The type of the throw expression is Nothing
. Note that the expression following the keyword throw
can only be an object of the type inherited from Exception
or Error
. The throw expression changes the execution logic of the program. When the throw expression is executed, an exception is thrown. The code block that captures the exception is executed instead of the remaining expression after the throw expression is executed.
The following is an example of using the throw expression:
// Catch with exceptionTypePattern.
let listTest = [0, 1, 2]
try {
throw ArithmeticException()
let temp = listTest[0] + 1 // Will never be executed.
} catch (e: ArithmeticException) {
print("an arithmeticException happened: " + e.toString())
} finally {
print("the finally block is executed")
}
After the throw expression throws an exception, it must be able to capture and handle the exception. The sequence of searching for the exception capture code is the reverse sequence of the function call chain. When an exception is thrown, the function that throws the exception is searched for the matching catch block. If the catch block is not found, the function execution is terminated, and the function that calls this function is searched for the matching catch block. If the catch block is still not found, the function execution is also terminated, and the function that calls this function is searched in the same way until a matching catch block is found. However, if no matching catch block is found in all functions in the call chain, the program executes the terminate function in the exception. As a result, the program exits abnormally.
The following example shows the scenario where exceptions are captured at different locations:
// Caught by catchE().
func catchE() {
let listTest = [0, 1, 2]
try {
throwE() // caught by catchE()
} catch (e: IndexOutOfBoundsException) {
print("an IndexOutOfBoundsException happened: " + e.toString())
}
}
// Terminate function is executed.
func notCatchE() {
let listTest = [0, 1, 2]
throwE()
}
// func throwE()
func throwE() {
throw IndexOutOfBoundsException()
}