Exceptions
-
Understanding exceptions: Exceptions are runtime errors or unexpected conditions encountered by your program. In structured languages like DBL or C#, exceptions disrupt the normal flow of an application. They can be caused by anything from attempting to open a file that doesn’t exist to trying to read beyond the end of an array.
-
The TRY-CATCH-FINALLY structure: Exception handling is typically implemented using a TRY-CATCH-FINALLY block. The TRY block contains code that might potentially cause an exception. The CATCH block is where you handle the exception, and the FINALLY block contains code that is always executed, whether an exception occurs or not. Here’s how it’s typically structured:
try
begin
;; Code that may cause an exception
end
catch (ex, @ExceptionType)
begin
;; Code to handle the exception
;; "ex" is the caught exception object
end
finally
begin
;; Cleanup code that always runs
end
endtry
-
Catching specific exceptions: It’s considered good practice to catch specific exceptions rather than a general exception, as this allows for more fine-grained error handling. You can have multiple CATCH blocks to handle different types of exceptions specifically.
-
The exception object: When an exception is caught, information about the nature of the error is encapsulated in an exception object (denoted as “ex” in the CATCH block above). This object can contain several properties that you can use to understand the error better, such as the cause of the exception, the stack trace, and more.
-
The FINALLY block: The FINALLY block is optional but important for resource management. Code within this block will always run, regardless of whether an exception was thrown. This characteristic makes it the ideal location for cleanup code, such as closing file streams or releasing any resources that were locked.
-
Throwing exceptions: Apart from handling exceptions, you can also throw your own exceptions using the THROW statement when you detect an unrecoverable state in your program. You can either throw a pre-defined system exception or a custom exception that you define.
-
Custom exceptions: If the predefined system exceptions aren’t sufficient, you can define your own exception classes. These are particularly useful to indicate specific errors that are unique to your application’s business logic.
Exception handling is about maintaining control over an application when abnormal conditions occur. It prevents the application from terminating abruptly, allowing it to handle the error gracefully, which might include informing the user, logging the error for future analysis, or gently shutting down the process. Remember, while it’s important to catch exceptions, overuse of exception handling can lead to code that’s difficult to understand and maintain. As a rule of thumb, use exceptions for exceptional conditions, not the normal flow of control. Below, I’ll outline two scenarios: one where using exceptions is almost universally considered appropriate, and another where avoiding exceptions is a better choice.
Scenario 1: When to use exceptions – handling file operations
Context: Imagine a scenario where your application needs to read data from a configuration file that might not exist or could be inaccessible due to permission issues or runtime changes.
Use of exceptions: In this scenario, using exceptions is appropriate. File operations are inherently prone to numerous unexpected issues:
- File doesn’t exist: The file you’re trying to read doesn’t exist.
- Permission issues: The application doesn’t have the necessary permissions to read the file.
- File locked: The file is being used by another process.
- I/O errors: There’s an issue with the disk, leading to read/write failures.
Attempting to handle all of these conditions using return codes and conditional logic would make the code cumbersome, less readable, and hard to maintain. An exception handling mechanism shines here, as it centralizes the error-handling code and cleanly separates it from the main business logic:
try
begin
;; Attempt to read the file
data fileContent = File.ReadAllText("config.txt")
end
catch (ex, @FileNotFoundException)
begin
;; Handle case when file doesn't exist
end
catch (ex, @UnauthorizedAccessException)
begin
;; Handle permission issues
end
catch (ex, @IOException)
begin
;; Handle other I/O errors
end
In this case, exceptions allow you to handle each error type explicitly and provide a clear, high-level view of the different error types you’re anticipating.
Scenario 2: When to avoid exceptions – validating user input
Context: Consider an application form where users input their age. The age must be a positive integer and less than 120.
Avoiding exceptions: Using exceptions to handle basic validation checks is generally frowned upon. For example, throwing an exception if a user inputs a non-numeric value or a number that’s out of the expected range is considered poor practice. This isn’t an “exceptional” scenario; users often make input mistakes, and these are predictable and expected situations.
Instead, you should use standard conditional logic to handle these cases because they are part of the normal flow of the application:
data age, int
data userInput = GetAgeInput()
if (!int.TryParse(userInput, age) || age < 0 || age >= 120) then
begin
;; Display an error message to the user
end
else
begin
;; Proceed with the age value
end
In this scenario, using exceptions would complicate the code without providing any clear benefit. It could also degrade performance, as handling exceptions is resource-intensive and should be reserved for truly exceptional conditions that the application cannot easily predict or prevent.
Stack unwinding
Regardless of whether you’re using .NET or Traditional DBL, when an exception is thrown, the runtime will start the process of stack unwinding. This involves stepping back through the call stack—the record of nested method calls—to find the appropriate exception handler (CATCH block). When an exception is thrown, the runtime begins to search for a method in the call stack with a CATCH block that can handle the exception. This search moves from the point where the exception was thrown up through the call stack, effectively “unwinding” it. Each method in the call stack (from the point where the exception was thrown to the method that handles it) is exited in a controlled manner, and any FINALLY blocks in those methods are executed. If the runtime reaches the top of the call stack without finding an appropriate exception handler, the program terminates. This mechanism ensures that resources are cleanly released and that the system remains in a known state, even when unexpected scenarios occur. To illustrate, we’ll use a scenario where we’re trying to process a file:
namespace UnwindingExample
public class FileProcessor
public method ProcessFile, void
filePath, string
endparams
proc
try
begin
data fileContent, string
ValidateFilePath(filePath)
fileContent = DoSomeStuff(filePath)
ProcessContent(fileContent)
end
catch (ex, @Exception)
begin
Console.WriteLine("An error occurred: " + ex.Message)
end
endtry
endmethod
private method ValidateFilePath, void
filePath, string
endparams
proc
if (string.IsNullOrEmpty(filePath))
begin
throw new ArgumentException("File path cannot be null or empty.")
end
endmethod
private method DoSomeStuff, string
filePath, string
endparams
proc
try
begin
mreturn ReadFile(filePath)
end
finally
begin
;; Do some cleanup
Console.WriteLine("Cleaning up in DoSomeStuff")
end
endtry
endmethod
private method ReadFile, string
filePath, string
endparams
proc
;; Let's assume an exception is thrown here because the file doesn't exist.
throw new NoFileFoundException()
endmethod
private method ProcessContent, void
content, string
endparams
proc
;; Process the file content
endmethod
endclass
endnamespace
In this code:
ProcessFile
method is called to start the file processing.- Inside
ProcessFile
,ValidateFilePath
is first called to check the validity of the file path. - Next,
DoSomeStuff
is called to perform some operations on the file. - Inside
DoSomeStuff
,ReadFile
is called to read the file content, but let’s assume the file doesn’t exist, so aNoFileFoundException
is thrown.
At this point, stack unwinding begins due to the unhandled NoFileFoundException
:
- The runtime starts the unwinding process, looking for a CATCH block that can handle a
NoFileFoundException
. - It first checks the current method,
ReadFile
, but doesn’t find a suitable exception handler. - The runtime then leaves the
ReadFile
method and moves to the previous method in the call stack, which isDoSomeStuff
. - In
DoSomeStuff
, it finds no CATCH block to handle theNoFileFoundException
, but it does find a FINALLY block. The FINALLY block is executed, and the runtime continues unwinding inProcessFile
- In
ProcessFile
, it finds a CATCH block that handles the baseException
type (which can catch any exception). TheNoFileFoundException
is successfully caught here, and the error message is printed to the console. - The FINALLY block, if it existed within the TRY-CATCH block of
ProcessFile
, would execute after the CATCH block, regardless of whether an exception was caught.
During this unwinding process, the stack is effectively being “unwound” from the point of the exception (inside ReadFile
) back up through the nested method calls (DoSomeStuff
and ProcessFile
) until it finds an appropriate handler. If more method calls were nested, the runtime would continue unwinding through them in the same manner.
Note about local calls
There’s really no interaction with local calls during stack unwinding. The compiler will not allow you to call a local procedure within a TRY-CATCH block. The error DBL-E-LBLSCOPE tells you that the label is out of scope. While it is technically correct, it could be more informative.
Exceptions in legacy code
An error list is more efficient and can be easier to fully understand when working with Synergy I/O statements. ONERROR is nasty business, and you should avoid or rework it whenever it’s possible to do so safely. TODO: explain that all error handling can be converted to exceptions. Give a good example of onerror -> exception and error list -> exception
Defining custom exceptions
TODO: Explain how to define custom exceptions and why you might want to do so.