28

I want to add error handling to:

var firstVariable = 1;
var secondVariable = firstVariable;

The below won't compile:

try
{
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Why is it necessary for a try catch block to affect the scope of variables as other code blocks do? 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?

9
  • 15
    A 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.
    – Neil
    Commented Jul 19, 2017 at 10:45
  • "is a specific type of code block". Specific in what way? Thanks Commented Jul 19, 2017 at 10:48
  • 8
    I mean anything between curly brackets is a code block. You see it following an if statement and following a for statement, though the concept is the same. The contents is in an elevated scope with respect to its parent scope. I'm pretty sure this would be problematic if you just used the curly brackets {} without the try.
    – Neil
    Commented Jul 19, 2017 at 11:33
  • 1
    Tip: Note that using(IDisposable) {} and just {} applies similarly too. When you use a using with an IDisposable, it will automatically clean up resources regardless of success or failure. There are a few exceptions to this, like not all classes you'd expect implement IDisposable... Commented Jul 19, 2017 at 17:48
  • 2
    Lots of discussion on this same question on StackOverflow, here: stackoverflow.com/questions/94977/… Commented Jul 20, 2017 at 0:59

3 Answers 3

96

What if your code was:

try
{
   MethodThatMightThrow();
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Now you'd be trying to use an undeclared variable (firstVariable) if your method call throws.

Note: The above example specifically answers the original question, which states "consistency-sake aside". This demonstrates that there are reasons other than consistency. But as Peter's answer shows, there is also a powerful argument from consistency, which would for sure have been a very important factor in the decision.

17
  • Ahh, this is exactly what I was after. I knew there were some language features that made what I was suggesting impossible, but I couldn't come up with any scenarios. Thanks very much. Commented Jul 19, 2017 at 10:34
  • 1
    "Now you'd be trying to use an undeclared variable if your method call throws." Furthermore, suppose this were avoided by treating the variable as though it had been declared, but not initialized, before the code that might throw. Then it wouldn't be undeclared, but it would still be potentially unassigned, and definite assignment analysis would prohibit reading its value (without an intervening assignment that it could prove occurred). Commented Jul 19, 2017 at 18:25
  • 3
    For s static language like C#, declaration is really only relevant at compile time. The compiler could easily move the declaration earlier in the scope. The more important fact at runtime is that the variable might not be initialized.
    – jpmc26
    Commented Jul 19, 2017 at 21:52
  • 3
    I disagree with this answer. C# already has the rule that uninitialized variables cannot be read from, with some dataflow awareness. (Try declaring variables in cases of a switch and accessing them in others.) This rule could easily apply here and prevent this code from compiling anyway. I think Peter's answer below is more plausible. Commented Jul 20, 2017 at 8:55
  • 2
    There is a difference between undeclared an uninitialized and C# tracks them separately. If you were allowed to use a variable outside the block where it was declared, it would mean you would be able to assign to it in the first catch block and then it would be definitely assigned in the second try block.
    – svick
    Commented Jul 20, 2017 at 10:43
67

I know that this has been well answered by Ben, but I wanted to address the consistency POV that was conveniently pushed aside. Assuming that try/catch blocks didn't affect scope, then you would end up with:

{
    // new scope here
}

try
{
   // Not new scope
}

And to me this crashes head on into the Principle of least astonishment (POLA) because you now have the { and } doing double duty depending on the context of what preceded them.

The only way out of this mess is to designate some other marker to delineate try/catch blocks. Which starts to add a code smell. So by the time you have scopeless try/catch in the language it would have been such a mess that you would have been better off with the scoped version.

5
  • Another excellent answer. And I'd never heard of POLA, so nice further reading. Thanks a lot mate. Commented Jul 19, 2017 at 13:40
  • "The only way out of this mess is to designate some other marker to delineate try/catch blocks." - do you mean: try { { // scope } }? :)
    – CompuChip
    Commented Jul 19, 2017 at 22:16
  • @CompuChip that would have {} pulling double duty as scope and not scope creating depending on context still. try^ //no-scope ^ would be an example of a different marker.
    – Leliel
    Commented Jul 20, 2017 at 3:30
  • 1
    In my opinion, this is the much more fundamental reason, and closer to the "real" answer. Commented Jul 20, 2017 at 9:01
  • @JackAidley especially since you can already write code where you use a variable that might not be assigned. So while Ben's answer has a point about how this is useful behavior, I don't see it as why the behavior exists. Ben's answer does note the OP saying "consistency's sake aside", but consistency is a perfectly fine reason! Narrow scope has all sorts of other benefits.
    – Kat
    Commented Jul 25, 2017 at 21:32
23

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.

1
  • "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." What the compiler cares about is that control can reach later statements even if previous statements throw an exception. The compiler normally assumes that if one statement throws an exception, the next statement won't be executed, so an unassigned variable won't be read. Adding a 'catch' will allow control to reach later statements - it's the catch that matters, not whether the code in the try block throws. Commented Jul 21, 2017 at 12:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.