Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Variables initialized in a try-block are awkward to use going forward #1884

Open
Jetz72 opened this issue Oct 4, 2021 · 6 comments
Open
Labels
request Requests to resolve a particular developer problem

Comments

@Jetz72
Copy link

Jetz72 commented Oct 4, 2021

Apologies in advance if this has been discussed before; I did look around a bit but didn't find anything.

A common situation I encounter while programming is this:
var foo = functionThatMightThrow();

When this happens, there are generally two options:

var foo;
try {
  foo = functionThatMightThrow();
}
catch(e, stackTrace ){
  //Exit (via return, throw...), or set foo to something else.
}
//rest of the code using foo

- or -

try {
  var foo = functionThatMightThrow();
  //rest of the code using foo
}
catch(e, stackTrace) {
  //Exit, or do something not involving foo
}

The former awkwardly splits the declaration and assignment. Not a huge problem but not all that pretty either, with a whole block for a single line and an extra line for the declaration. The latter is alright if what comes after is just a few lines of code but quickly becomes hard to read beyond that.

It'd be nice to have some syntactic sugar to clean this up a bit, e.g.

var foo = try functionThatMightThrow(),
catch(e, stackTrace) (/* Other value for foo, throw, or return */);
//rest of the code using foo

Bit dodgy on the exact syntax there.
Or perhaps something similar to the negative if-variable syntax discussed in #1201, where you declare the variable inside the statement, follow with a contingency that exits the enclosing context, then the variable is in scope for the rest of the block.

try(var foo = functionThatMightThrow())
catch(e, stackTrace){
  //Must exit via return, throw, break...
}
//rest of the code using foo

Not dead-set on any particular solution here; maybe some other language has a better way of doing this cleanly. Would be interested in hearing others' thoughts on this though.

@Jetz72 Jetz72 added the request Requests to resolve a particular developer problem label Oct 4, 2021
@Levi-Lesches
Copy link

I see where you're coming from, but honestly I don't think this is necessary. There are perfectly valid reasons to need to declare variables specifically in the try scope, in the catch scope, and in the enclosing scope separately. Consider something simple like:

int computeResult() { 
  // First, get a number from user input
  int number;  // <-- this is needed throughout the function
  try {
    String input = getUserInput();  // <-- this is not needed outside the try
    number = int.parse(input);
  } on FormatException catch(error, stack) {
    int numCalls = stack.toString().split("\n").length;  // <-- not needed outside the catch
    print("The stack trace is $numCalls lines long");
    return -1;
  }

  // Now do the real logic
  int result = 0;
  for (int i = 0; i < number; i++) {
    result += i * 2;
  }
  return result;
}

Not to mention, the analyzer is smart enough to realize when number is and isn't assigned to. I think that by moving declarations around, you're stating your intentions as to how you're going to use the variable, which helps the reader know what to expect. It's like a topic sentence for try/catch blocks.

@lrhn
Copy link
Member

lrhn commented Oct 4, 2021

An expression-catch is interesting (so is an expression-switch).
It's basically a way to reduce the control flow of the try/catch statement and possible variable assignment flows, etc., down to a single value coming out, one way or another. It's not new, SML has a handle operator that applies to any expression.
We could do:

 e1 catch (e) e2

(the grammar should work out since catch is a reserved word). All we need to do is choose a precedence for the operator.
Doing

try e1 catch (e) e2

instead makes it even simpler, but might as well just require parentheses instead of the try then.

The big issue with try/catch and variables is that we cannot assume that any code inside the try/catch block has been executed. The first operation to be evaluated might throw (because anything might throw), so:

try { 
  .... anything ...
} catch (e) {
  // Can't assume anything here
}
// Or here.

anything happening inside the try must be assumed to maybe not have happened. No variable declared has been created, or if so, not initialized.
So, nothing happening inside a try can be trusted after the try.
(Sure, you might think that var x = 2.5; is completely safe, but that 2.5 is a double, which is heap allocated, and just maybe we'll throw an OutOfMemoryError allocating it. Better to assume nothing!)

That's different from other code structures because if something throws inside those, the rest will be skipped too. We can assume that if we get to the following code, the prior code didn't throw. A catch and finally is different, because you can always get there, no matter what happened before.
Interestingly, if the catch block always throws, code after the try/catch can assume that the try block didn't throw. (I don't know if we take advantage of that.)

@Levi-Lesches
Copy link

We could do:

 e1 catch (e) e2

Something along the same lines has been proposed before. The comparison was that since ?? can switch between expressions based on the presence of null, !! (or something similar) should be able to choose expressions based on the presence of an error

int number = int.parse(userInput) !! -1;

Personally, I don't love it. Why do that when the much more familiar alternative exists?

int number = int.tryParse(userInput) ?? -1;

I think it's simpler for functions that expect certain exceptions to happen often to just return null in that case and let the existing null-safety mechanics kick in. The question of whether variables have been declared or initialized could be solved by changing it to "is this variable null" and using null-aware operators where necessary.

In more complex cases where a variety of errors can occur and need special treatment, the full try/catch should be used instead.

@gosoccerboy5
Copy link

Why not just an out-of-scope/function-scoped variable declaration like Javascript's var? Or would that encourage bad programming or be difficult for the compiler in some way?

@Levi-Lesches
Copy link

function-scoped variable declaration like Javascript's var?

Dart already allows you to scope a variable to its enclosing function, you just need to move the declaration outside the try/catch.

@Jetz72
Copy link
Author

Jetz72 commented Oct 5, 2021

Why do that when the much more familiar alternative exists?

int number = int.tryParse(userInput) ?? -1;

tryParse is a pretty handy tool to have, but it's not a pattern implemented universally. Whether or not a function will have a dedicated tryWhatever variant is up to whoever writes the API unless the developer wants to start adding extensions. I'm liking the sound of having an inline catch expression in the language to generalize the concept. It helps avoid the code smell of conflating null with failure. I could see someone falling into the habit of making every exception-prone function nullable and just using that to indicate an error so that the fallback is quick and easy to write (also impossible to overlook - the nullable return type would kinda cause it to work like a checked exception. Hmm...)

I dunno how common this is, but I could also see it helping in my proclivity to include as much detail as possible when setting up exceptions:

var lengthyComplicatedProcess(var input)
{
  var foo = try(generateFoo(input)) catch(e) onError(e, input);
  var bar = try(generateBar(foo)) catch(e) onError(e, input, foo);
  var baz = try(generateBaz(bar)) catch(e) try(getFallbackBaz()) 
       catch(e2) onError(e, input, foo, bar, e2);
  return baz;
}
Never onError(var e, [var input, var foo, var bar, var e2])
{
  //Update state, cleanup fields, notify a callback, etc.
  //Either rethrow e, or maybe e2, maybe composite everything we got into a new dedicated exception and throw that...
}

That'd take four separate try/catch blocks currently, either nested inside one another or with a list of declarations in front of them. I'd more likely just end up wrapping the whole thing in one try block and hoping I can later reproduce the issue with just input and whatever state info or fields I guess could be relevant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

4 participants