Consistency-sake aside, wouldn't it make sense for us to be able to wrap our code with error handling without the need to refactor?
To answer this, it's necessary to look at more than just a variable's scope.
Even if the variable remained in scope, it would not be definitely assigned.
Declaring the variable in the try block expresses--to the compiler, and to human readers--that it is only meaningful inside that block. It is useful for the compiler to enforce that.
If you want the variable to be in scope after the try block, you could declare it outside of the block:
var zerothVariable = 1_000_000_000_000L;
int firstVariable;
try {
// Change checked to unchecked to allow the overflow without throwing.
firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
}
That expresses that the variable may be meaningful outside the try block. The compiler will allow this.
But it also shows another reason it would not usually be useful to keep variables in scope after introducing them in a try block. The C# compiler performs definite assignment analysis and prohibits reading the value of a variable that it has not proved has been given a value. So you still cannot read from the variable.
Suppose I attempt to read from the variable after the try block:
Console.WriteLine(firstVariable);
That will give a compile-time error:
CS0165 Use of unassigned local variable 'firstVariable'
I called Environment.Exit in the catch block, so I know the variable has been assigned prior to the call to Console.WriteLine. But the compiler does not infer this.
Why is the compiler so strict?
I cannot even do this:
int n;
try {
n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}
Console.WriteLine(n);
One way to look at this restriction is to say definite assignment analysis in C# is not very sophisticated. But another way to look at it is that, when you write code in a try block with catch clauses, you are telling both the compiler and any human readers that it should be treated like it might not all be able to run.
To illustrate what I mean, imagine if the compiler allowed the code above, but then you added a call in the try block to a function that you personally know won't throw an exception. Not being able to guarantee that the called function didn't throw an IOException
, the compiler could not know that n
was assigned, and then you would have to refactor.
This is to say that, by foregoing highly sophisticated analysis in determining if a variable assigned in a try block with catch clauses has been definitely assigned afterwards, the compiler helps you avoid writing code that is likely to break later. (After all, catching an exception usually means you think one might be thrown.)
You can make sure the variable is assigned through all code paths.
You can make the code compile by giving the variable a value before the try block, or in the catch block. That way, it will still have been initialized or assigned, even if the assignment in the try block does not take place. For example:
var n = 0; // But is this meaningful, or just covering a bug?
try {
n = 10;
}
catch (IOException) {
}
Console.WriteLine(n);
Or:
int n;
try {
n = 10;
}
catch (IOException) {
n = 0; // But is this meaningful, or just covering a bug?
}
Console.WriteLine(n);
Those compile. But it is best to only do something like that if the default value you give it makes sense* and produces correct behavior.
Note that, in this second case where you assign the variable in the try block and in all catch blocks, although you can read the variable after the try-catch, you would still not be able to read the variable inside an attached finally
block, because execution can leave a try block in more situations than we often think about.
* By the way, some languages, like C and C++, both allow uninitialized variables and don't have definite assignment analysis to prevent reading from them. Because reading uninitialized memory causes programs to behave in a nondeterministic and erratic fashion, it is generally recommended to avoid introducing variables in those languages without supplying an initializer. In languages with definite assignment analysis like C# and Java, the compiler saves you from reading uninitialized variables and also from the lesser evil of initializing them with meaningless values that can later be misinterpreted as meaningful.
You can make it so code paths where the variable isn't assigned throw an exception (or return).
If you plan to perform some action (like logging) and rethrow the exception or throw another exception, and this happens in any catch clauses where the variable is not assigned, then the compiler will know the variable has been assigned:
int n;
try {
n = 10;
}
catch (IOException e) {
Console.Error.WriteLine(e.Message);
throw;
}
Console.WriteLine(n);
That compiles, and may well be a reasonable choice. However, in an actual application, unless the exception is only thrown in situations where it does not even make sense to try to recover*, you should make sure that you are still catching and properly handling it somewhere.
(You can't read the variable in a finally block in this situation either, but it doesn't feel like you should be able to--after all, finally blocks essentially always run, and in this case the variable is not always assigned.)
* For example, many applications do not have a catch clause that handles an OutOfMemoryException because anything they could do about it might be at least as bad as crashing.
Maybe you really do want to refactor the code.
In your example, you introduce firstVariable
and secondVariable
in try blocks. As I've said, you can define them before the try blocks in which they are assigned so they will remain in scope afterwards, and you can satisfy/trick the compiler into allowing you to read from them by making sure they are always assigned.
But the code that appears after those blocks presumably depends on them having been assigned correctly. If that is the case, then your code should reflect and ensure that.
First, can (and should) you actually handle the error there? One of the reasons exception handling exists is to make it easier to handle errors where they can handled effectively, even if that is not near where they occur.
If you can't actually handle the error in the function that initialized and uses those variables, then maybe the try block shouldn't be in that function at all, but instead somewhere higher (i.e., in code that calls that function, or code that calls that code). Just make sure you're not accidentally catching an exception thrown somewhere else and wrongly assuming it was thrown while initializing firstVariable
and secondVariable
.
Another approach is to put the code that uses the variables in the try block. This is often reasonable. Again, if the same exceptions you're catching from their initializers could also be thrown from the surrounding code, you should make sure you're not neglecting that possibility when handling them.
(I am assuming you're initializing the variables with expressions more complicated than shown in your examples, such that they could actually throw an exception, and also that you are not really planning to catch all possible exceptions, but just to catch whatever specific exceptions you can anticipate and meaningfully handle. It's true that the real world is not always so nice and production code sometimes does this, but since your goal here is to handle errors that occur while initializing two specific variables, any catch clauses you write for that specific purpose should be specific to whatever errors those are.)
A third way is to extract the code that can fail, and the try-catch that handles it, into its own method. This is useful if you might want to deal with errors completely first, and then not worry about inadvertently catching an exception that ought to be handled somewhere else instead.
Suppose, for instance, that you want to immediately quit the application upon failure to assign either variable. (Obviously not all exception handling is for fatal errors; this is just an example, and may or may not be how you want your application to react to the problem.) You could so something like this:
// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
try {
// This code is contrived. The idea here is that obtaining the values
// could actually fail, and throw a SomeSpecificException.
var firstVariable = 1;
var secondVariable = firstVariable;
return (firstVariable, secondVariable);
}
catch (SomeSpecificException e) {
Console.Error.WriteLine(e.Message);
Environment.Exit(1);
throw new InvalidOperationException(); // unreachable
}
}
// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
var (firstVariable, secondVariable) = GetFirstAndSecondValues();
// Code that does something with them...
}
That code returns and deconstructs a ValueTuple with the the C# 7.0's syntax to return multiple values, but if you're still on an earlier version of C#, you can still use this technique; for example, you can use out parameters, or return an custom object that provides both values. Furthermore, if the two variables aren't actually tightly related, it would probably better to have two separate methods anyway.
Especially if you have multiple methods like that, you should consider centralizing your code for notifying the user of fatal errors and quitting. (For example, you could write a Die
method with a message
parameter.) The throw new InvalidOperationException();
line is never actually executed so you need not (and should not) write a catch clause for it.
Aside from quitting when a particular error occurs, you might sometimes write code that looks like this if you throw an exception of another type that wraps the original exception. (In that situation, you would not need a second, unreachable throw expression.)
Conclusion: Scope is only part of the picture.
You can achieve the effect of wrapping your code with error handling without refactoring (or, if you prefer, with hardly any refactoring), just by separating the variables' declarations from their assignments. The compiler allows this if you satisfy C#'s definite assignment rules, and declaring a variable ahead of the try block makes its larger scope clear. But refactoring further may still be your best option.
try.. catch
is a specific type of code block, and as far as all code blocks go, you cannot declare a variable in one and use that same variable in another as a matter of scope.{}
without the try.