The real solution to this is to make so that blocks are expressions. So we can do: let double = try { maybeNumber() * 2 } catch { -1 } Rust got it right
Try scoping makes perfect sense. You're assuming that your catch block returns or throws. If it doesn't, then there's no guarantee that a value assigned in a try block isn't uninitialized. If your catch block didn't return in the first example, then your function would throw a TypeError because you can't multiply undefined by 2. Thus that line _can_ throw, and so belongs in the try block. Then you can look at the return statement. Unless you want your function to be able to return undefined, then you have an error in your logic. To fix the logic, move the return statement into the try block with its dependencies so that it doesn't run if its dependencies didn't resolve.
that's why errors as values are just indefinitely better than exceptions and try/catch also if you've watched to the end he discovered that and recognized the scoping makes sense how it is
JavaScript can't type error in multiplications, the only way you can error in a multiplication is if one of the factors's prototype's valueOf function throws in the video's case, undefined * 2 is NaN
Scoping is necessary and correct. It is plain skill issues. The calculation based on the throwable function must be in the same scope. Putting it outside makes no sense. The design to return errors or throw them is equaliy good or bad. The problem in Javascript is the handling of the function definition (you may not know that a function throws). It could be changed inspired by swift, needing to add a throws section to the function definition if a function actually uses the throw statement. This example tries using try-catch in errors-as-value manner. Just don't!
6:20 Writes a custom error "Forcing var to never get hit" -> Sees the error in the logs -> Interesting that they have "Forcing var to never get hit" error 😂😂😂
It would be a strange break of consistency to have one particular block not be scoped. I'm looking at this from the perspective of C-like languages, where just using plain brackets alone serves no other purpose than to introduce scope.
@@balduran It can make sense sometimes in C++ (probably Rust too) by allowing you to use block scope to control the lifetime of objects using RAII. In languages without RAII there is no reason to use it.
javascript devs discover in 2024 why try/catch statements create separate scope 🤣🤣🤣 it was designed in this way for Java and C++ for a very good reason. And most likely there was some small unknown language that had the same concept 10+ years before Java was even created
my exact thoughts, like dude that's not a js issue, it's an oop design thing. and as error handling goes, i'm loving how go/rust/zig treat errors as values, much pleasant to work with.
are you even following the discussion? it's more so about function scope vs. block scope, not scope in general. considering JS has embraced both design paradigms at some point it's not crazy to see some people weigh the pros/cons of both
After my daughter's first day of school. She came home and declared "zero is the first number". I was happy she was on the right team. A week later she asked: "Dad are you sure that if you count long enough you will not get back to zero". I said "yes I'm sure". She wasn't buying it and asked me multiple times. At that point I tried to settle with fact that my daughter probably would become a C-developer.
@@05xpeter In fact you are not sure. There are at least half as many algebras in which counting upward would get one back to zero. This is equally real and possible in mathematics as the infinity you're indoctrinated with. Numbers aren't a physical thing, everything is a convention, and cyclic number systems are arguably even more rigorously defined than infinite sets, especially when it comes to integers.
@@milanstevic8424 Yeah there is always the possibility that see could become a mathematician. I guess I though about that when I named her "Tilde", my wife wan't happy when she found out that I had put a math easter egg in her name. But as I told her it had to be "Tilde" I couldn't call her "Line" (that's my wife's name) and "Cirkle" is a little to much.
Watch to the end - Theo realizes the scoping in `try {}` is needed after all! I hope Theo pins a comment about this. The problem: while declarations are hoisted in blocks, initializations are not. An error within a try can result in an initialization not being reached; therefore it is control flow.
An additional argument is syntax: when something looks like a block (surrounded by the curly brackets) why shouldn't it be a block? Any language I've used in my decades of coding handles it this way and it makes perfect sense.
Object literals use curly braces, but they're not a block per se. But I guess it's a bit different when it's an expression rather than a statement, which is usually easy to distinguish
This is the correct answer. I took the same pattern from Elixir, where try blocks have a return and their result can be assigned, making them effectively tiny functions with syntax sugar.
That'd one extra reason to love golang's errors as values implementation... Everything is so crystal clear because of that! (that and the fact that Go has even less syntactic sugar than C 🤣)
@@akam9919 Except that it's completely inconsistent with every other block scoping behavior in JS. Why would you make one single exception? That is setting yourself up for disaster.
This is exactly where I prefer the approach that Kotlin takes. It maintains the scoping but allows returns from the evaluated value of the try/catch flow. It would look a bit like the following in JS: const num = try { getNumber(); } catch (e) { console.log(e); return -1 }
I've liked Option/Result type of error handling. Especially with exhaustive pattern matching languages. Code is much more transparent on what it can return, increased type safety, and compiler can reason deeper about your code and catch more errors at compile time. Also gives you the lower level primitives to build custom higher level error handling more suited to your particular use case. Having to use `let` outside of the try/catch as a workaround and not being able to use `const` just feels dirty and unnecessarily imperative to me.
I feel like we would all be better programmers if we are forced to deal with error cases, enforced by the types, more often. It would be annoying, and maybe less efficient, but hopefully less breaking stuff :)
@@fjjfjfj Performance is an interesting point. There are probably scenarios where checks can be skipped. There's also scenarios where things like bounds checking of arrays don't need to be performed because the compiler can guarantee it's not empty. There's even ways to create types for non-empty arrays as well. I don't think we're there yet, but I've always wondering if compiler technology improves and we can specify our intent at higher levels, the compilers can make more assumptions about the code and optimize better. Really hard to do with imperative / procedural code though.
The main shortcoming of the try catch syntax is that it isn't an expression. In practice it forces the pattern of isolating the block in another function so that one can return something useful in both try and catch blocks. e.g. try { return getUnsafeStuff() } catch (err) { return getAlternativeStuff() } A new expression syntax would help. But I've people using a helper like this: const result = orElse(() => getUnsafeStuff(), err => getAlternativeStuff())
This is why people like errors as values. It's not that returning an error is much different than throwing it, but the fact that you only scope the error case outside of the function is what people like.
The fact that a substantial number of people think that curly braces should sometimes create a new scope and sometimes not is honestly kind of insane to me. The reason var works is not because it hoists but because of inconsistent scoping rules like this. The hoisting part has nothing to do with.
Another reason to love Rust with match assignment destructuring and Result monad. Dealing with failable code while assigning something is a piece of cake.
@6:23 Theo finds his own error message interesting 😂 Love these javascript vids. Catharsis all wrapped in a senseless block. You should do one on defining objects with variables for keys. That syntax is so confusing where the key can be dropped or wrapped in brackets, or spread, but only shallowly spread... And there's no built-in optimised deep copy! I have to trust a util or write a case for every symbol, collection, set, array, etc, etc if I plan to reuse it lol
If the block scope of the function leaks out of the try then throwing midway through would result in uninitialized variables. With using let outside and reassigning you have an explicit initialization which can be set to a new value if the try succeeds. If you hoist the assignments out you either need var semantics where everything is initialized to undefined at the start of the function and then changed wherever the var statement occurs. Uninitialized state is very unsafe, especially with block-scoped variables as they are initialized at statement position not hoisted to the top of the block, so variable initialization might not be reached and then subsequent code which assumed initialization happened could corrupt memory and crash the process. Unscoped try/catch wouldn’t work the way you think it should.
I think it's also worth covering that it could also break on the line that does the assignment, but still *before* assignment, depending on what it does. If `const foo = MightThrow()` then `MightThrow()` throwing would occur before assignment as function execution takes place before assignment. I.e. don't think of it as every separate line potentially failing so much as every separate operation within every line.
This is actually something I've searched about as I thought the very same thing as you. And luckily, Anders Hejlsberg (C# & TS creator) has talked about it. His opinion was that although beneficial for your code, integrating this with 3rd party libraries would be really bad DX as you would have to handle a bunch of cases that you may not even be interested about from these libraries. Of course his opinion was much deeper than that, but that's the overall opinion I got from him. I don't wanna link anything here as I don't know how youtube treats comments with links (bots are allowed but actual people not, huh?). But you can look it up as "The Trouble with Checked Exceptions A Conversation with Anders Hejlsberg, Part II" it's an article from "Artima" where they interview uncle Anders. It's a good and recommended 5min read :)
I wished that once too, and then I tried java which statically analyses exceptions. It's very annoying to deal with. I think its just better to have one catch-all statement to handle all potential runtime exceptions. (But if if it was an optional feature of typescript I would still like it for some use cases)
@@panmateusz It's a little more complicated in Java. Initially Java used so-called checked exceptions and handling was mandatory. Either by try/catch or using the 'throws' keyword after a method signature with all exceptions this method could throw. Well, that decision didn't age very well and runtime exceptions were introduced. They work basically like JS errors. Older parts of the stdlib and some libraries still use checked exceptions, more modern parts and most libraries use runtime exceptions. This is also very annoying. I'm really thankful that JetBrains dropped checked exceptions in Kotlin completly. There's an annotation available instead of the keyword 'throws', but it's not mandatory to use it. But Kotlin allows you to not use exceptions at all. Thanks to sealed classes and some other language features, it's very easy to implement a result object for handling success and error states (Kotlin has a native result class, but IMO it's not that great, so I wrote my own). It's not as convenient as with Rust, but it works fine, makes error handling easier and forces you to handle errors. It's a little verbose at times, sometimes annoying, but in the end, a way better error handling with less potential for bugs. And at least for my taste, nicer than Go's approach will tuples and the omni-present null-checks of err.
With a lot of people doggedly holding on to their various opinions, it's honestly good to see someone change their opinion while working through something. Honestly based.
I don't know why this wasn't brought up, but the solution is absolutely not to have the variable be hoisted, the solution is to have try (and if and other control flow) as an expression. const num = try { getNumber() } catch (e) { return -1; }; const double = num * 2; Rust does this and it's excellent. I don't even think this would interfere with the current JS grammar since try as an expression is illegal, and the return of the last value of a try statement is a noop so equivalent to the old behaviour.
@@theairaccumulator7144 I don't think it's worse, atleast it doesn't indent the code for no reason. But I agree it might get more chatty if you need to handle several errors.
@@steadexe You should place as little as possible inside your try-catch. If your entire code is wrapped in a try-catch then that's just a skill issue. The indentation should be pretty much the same as what "if err != nil" causes. In fact, wrapping as little as possible in try-catch is preferable since if you have a lot of your code wrapped in it, you might catch unexpected and unintended errors which could be problematic in many ways. You also want to handle errors as quickly as possible and restore to a normal state, or exit with an expected failure state rather than an unknown one.
I mean, anything that might depend on unreliable (read: may fail/error) code is also, by definition, unreliable. A chain is only a strong as its weakest link. Therefore, it makes more sense to put the lines that may throw, and everything else that depends on it inside the try block, especially since in this simple case it just conditionally doubles the value and returns it with no extra dependencies. This solves the problem of trying to access an out-of-scope variable, and also keeps the code better organized, and easier to read and maintain. Keep in mind, I/O failure is also a thing. Your file may succesfully open, but that doesn't necessarily mean it'll succesfully read, or write. Might as well group all operations involving the same unreliable foreign resource into a single try-catch block. Also, var hoisting is a mistake and should be violently ignored moving forward. It's only kept around for backwards compatibility with old, broken code. Please, just pretend it doesn't exist. The only circumstance one could possibly prefer var hoisting is to use it instead of the finally block, to check-not-undefined and then free resources. But it's ridiculous, no sane person would prefer to declare, but not define a variable at the top of the scope, IMPLICITLY. And if you think you found a legitimate usecase, please just study more. There's a reason why this behavior does not exist in other languages, and if you want to declare, but not define a variable at the top of the scope, you have to do so explicitly. If it wasn't because mistakes were made and old JS must still continue working, let would've replaced var.
Hot take: that's how things probably should be, any part of computer system should be able to evolve through change (except for null being an object, obviously, that should stay forever this way). Javascript was mediocre before it become good.
I'm not sure why this wasn't mentioned, but `finally` helps a lot when writing `try/catch` in JS. There are a lot of control flow issues that you can solve there. It allows you to write some code after that will execute even if the failure happens.
@@captain.america1123I would argue that since finally will execute regardless of what happens with the try/catch, it *can* be a perfect place to self-contain a proper response But the use-case of a finally block is already quite situational in my opinion, and more suited to the tasks you mentioned (db transactions, file handling closure, removing global variable pollutants, etc) So can != should, but programmers gonna program
I definitely agree that it’s not something that should be used _all_ of the time. In retrospect, my original comment shouldn’t have called out the “control flow” piece specifically. I only meant to draw attention to the third part of “try/catch” that isn’t discussed much which can be used to solve _some_ issues and help with writing cleaner code.
I just pull those kinds of short try-catch variable definitions into their own function and suddenly it's no longer a problem. It can even be a local function within the function that needs to do it.
lol I loved the change of mind when you figure it out. Honestly, you can get away with a lot of things mixing the `try/catch` syntax and the `.catch()` syntax when necessary and it's really ok. What bothers me the most with exceptions is that they are not typed. In java, we declare a function saying what the function can throw so the catch block is also typed. This is not the case with Typescript and I believe it sucks
I've read about hoisting and understood it and then almost completely forgot about it for so many times in the past 8 years that I can't even keep count anymore.
I started pulling my hair out thirty seconds into this video but by the end was so fucking happy. Not just that you flipped sides (although as you say the DX does suck) but because it takes a distinct absence of ego to call BS on your own position after spending nearly ten minutes explaining it in public. If only more debates and conversations were had in good faith like this. Alright I've been watching you for like two years ago but because of this particular one, yeah okay, I'm finally clicking the fucking sub button.
In JS you need to think of try-catch as something that protects a whole scope, like the whole function. Happy path should happen in try{} and Failure path should happen in catch{}. If something always needs to happen last it must happen in finally{}
This makes sense if you take the return out of the catch. Anything which is then hoisted could be an error outside of the try catch. Accessing the variable again would mean that you are now referencing the error object
Having the try/catch statements both be something that is considered their own code blocks is honestly something I agree with. Try only runs code up until an error is thrown, in which case it then runs the catch code, so if you define a variable within a try block but it crashes before it reaches this definition, then using this undefined variable outside the try statement wouldn't be possible. I also like the brackets counting for consistency, where every block created with {} is its own block. It would be really strange if try was the only block with brackets that didnt create a scope, and since it creates scope it prevents the accessing of a variable that doesn't exist.
I think try scoping makes total sense. The actual solution to the ergonomic issues created by that aren't solved by "removing" the scoping, but one option would be to make "try/catch" an expression (i.e. it returns a value, can be assigned to a variable). Something like this: const num = try { getNumber() } catch (e) { console.log(e); -1 } This is the way Kotlin works, and makes code very nice and readable. However, I'm not sure if this syntax is really fit for JS to begin with (the implicit return), so there's that. Bonus: it's also very TypeScript friendly, as it can automatically make a union type out of the return values for each of try and catch blocks (or `T | undefined` if one of them doesn't return).
Yeah I don’t see why it’s bad either. If you want to handle errors from the other function call then why not put a separate try/catch in that other function?
The problem it separates the error handling from the point where the error arises. As the function becomes longer, this problem grows. The solution is typically to factor out the part of the function that can fail into a new function that includes the error handling.
There's other languages that handle this in a very unique way, that I think would be doable in typescript. For example Scala's Try Basically it's like a try catch block, but without catch. The catch happens in the type definition. So any exception thrown is saved in the return type. The simplest example is a function to convert a string to number: def parseInt(s: String): Try[Int] = Try(s.toInt) Now the result of the function parseInt is of type Try, which already tells you (much more explicitly than the ghostly typescript errors) that this number that you get returned is really inside this Try wrapper which can fail, which forces you to deal with the error before accessing it, much like Go with their err != nil spam. Just that in Scala, you can decide when to unwrap and deal with the error, instead of doing it right away. With TypeScript having such a strong type system, I'm surprised that it doesn't have anything like this for error handling
JS does that because it create a new scope whenever it encounter { }, you can even do something like main:{ // something somethig } and it will be an entire new space there. of course the facking var will escape it.
Agree on principle but would be confusing that this block differs from other blocks. Value in being consistent. PS. Did not know about "var-hoisting" :D
It's good that you came around to the correct way of thinking that this way of it working makes sense *and* that JavaScript sucks. Another example for you, consider a normal block in a language like C: int main ( void ) { int a = foo(); int b = bar(); { int c = a + b; } printf( "%d ", c ); return 0; } // will not work because `c` had scope only within that inner block, but would if the printf() call was made inside that inner block. This is an overly simplistic example, but it demonstrates the point all the same that a variable can leave an inner scope.
It's imo good dx that blocks work consistently. You know what you are getting, regardless whether you got an if, try, function or => before those { } Also a TH-camr saying "x% of my viewers aren't subscribed" is an automatic unsubscribe
In Python a try block can have an else block, which only runs if there was no exception thrown. With proper scoping that would make even more sense than in Python. So you could do: try { const x = foo(); } catch (e) { // cannot use x } else { bar(x); } finally { // cannot use x } // cannot use x
I kinda see what you mean, but I'd argue this makes even less sense in Python and is in fact one of the reasons that: 1 - variables are not declared; 2 - not every block suite creates a new scope. What I mean is that the confusion on what's defined were in the control flow in general is something to avoid as a design principle, which leads to the above two points. Anyway, I keep trying to give JS a chance, but...
aaannnd this is why i kinda dislike python... an esoteric block of code that can only run if there was no exception and conceptually merges if and try blocks? shouldn't the code that only runs if an exception wasn't thrown be just the rest of the function? just pre-set your variables outside the scope when it applies people, is not the end of the world
Another solution would be a try expression where the try block can return a value (or return from the function). Then you can declare the variable to be set as const.
@@Lambda_Ovineesoteric block of code is kinda funny ngl. There are else statements for both for and while loops in python though, so having it for try is predictable
On the other hand, "every code block is a contained scope" is good in its simplicity. It'd be nice if there were some alternative that doesn't require a closed block though
Bruh 10:05, if you know try block is not going to fail then keep things in the try block, wth on about there. Also it is not that try block is a control flow in and of itself but anything is a scope when you put them inside {}. You can create scope with {} without using any keyword, you know that right?
Explicitly defining the variable outside makes it obvious that it needs to be handled to people coming in later. The better answer is the ability to use try as an expression. Theoretically no scope could make sense in a different operator where catch guarantees you leave the function.
Honestly I see no issues here, thank God theo changed his mind. I spend most of my time coding with Python where you'd catch a named exception depending of the event that caused the exception, the only reason why I see this as an issue is where you have deeply nested throw statements when your function is calling another function that calls a function that throws an exception. Thinking about it makes me nervous. 😂
Applying normal scoping to a try block encourages you to put code in the try that doesn't relate to the error condition that the catch is intended to deal with, but which needs to access values computed in the try. I prefer for try blocks to be as small as possible, and only cover the code which the catch deals with, but that then complicates getting data out of the try block because of the scoping rules.
I think anyone who hates JS should go try to write their own language. I did and it made me realize how elegant JS is in its simplicity and orthogonal core concepts. Also you come to intuitively understand all kinds of low-level things like why try-catch is block-scoped, just like Theo did in this video.
When I heard your intro claim to be a "spicy" take I was like how spicy can a take be on try/catch? I guess not all heroes wear capes! But then you explained and yeah the scoping on try catch has been a huge annoyance! Not sure why that would be an unpopular opinion though. Will the pro-try/catch scoping mob come after you with pitchforks? lol.
Honestly, I believe the way it's setup is correct (given how JS works). If a for-loop has the same behaviors, why should try-catch be an exception? If the first baseman catches a ball thrown by the pitcher before you get there, you're OUT. You don't get to keep going all the way to the umpire/home plate - you're no longer active or assigned any role or position until you get called to bat again. If you're trying to define variables inside a try-catch but use them outside of it, then at least figure out why you're doing that instead of arguing about the implementation no different from other scopes. Obviously what I've said does not apply to other languages that were designed with these things in mind, but since JS already has a definition of a "scope" then it only makes sense to follow the standard, not create an entirely new one to please a few people.
just for the sake of clarity around the 3:00 mark: obviously, moving `const num` into the `try {}` block while leaving `const double` outside of said block will break, because that's how ES6 and the "block-scoped" `let` and `const` were designed to work. these did not replace `var` as everybody loves to tout. `try`/`catch` was around long before ES6, if you use `var num` (which is not block-scoped) inside the `try { }` block, then `num` is accessible outside of the `try`/`catch`, and you won't get a "ReferenceError: num is not defined"... this is only an issue today because we have different "building blocks" to play with, blocks that the designers of the other, older "blocks" did not foresee. my point is: when you use `var`, then `try`/`catch` makes sense. if you use `let` and/or `const`, then it does not.
Rust handles this so nicely. But in js i would love a try catch that looked like a ternary where the second value is a function with an exception argument. If the first returns a value, the second must also. That way variables still end up getting set.
Hmm for me it actually makes perfect sense, it is expected part of the try clausule is not executed(completely) so variable declarations can not be assumed after the try clausule.
This is the exact reason why they made var hoist variables. Beginners declaring variables within if blocks and wondering why they cannot access the variable outside the if block.
I think the better solution to using an uninitialized variable is for it to be an error to use a variable whose var statement hasn't been reached, just like it is for a let or const.
Just put each and every potentially undefined handle (database connection or rather a grab from some connection pool etc.) inside dedicated and properly scoped semantic codeblock, no?
Try-catch in JS is a mess for many reasons. My favorite pain points are scoping, as mentioned, as well as having to do if instanceof checks for different types of errors, and possibly worst of all not knowing which errors are potentially thrown at any given time. Considering you can do like in C++ and throw literally any value, it makes it very difficult to catch the errors gracefully because `error` will always be `any` type. It's just a lot of boilerplate to deal with.
I use this HOC a lot because of how ugly try/catch is: const safe = (fn, valueOnError) => (…args) => { try { return [fn(…args), null]; } catch(e) { return [valueOnError, e]; } }
Yes, but no. while try scoping can be painful to use in some or many cases, it would be more confusing to have blocks (code between curly braces) act differently when it's try and normally everywhere else
If there was some keyword other than var for this, once people were familiar, I'm sure it would be fine. For compiled languages, I've also seen the concept of flow scope, where the variable can still be accessed if nothing can potentially cause it to be uninitialized.
It should be changed to use the try statement where an error will occur, and have a catch block, that must return, follow that try statement. That way you're forced to handle the error if you need a value set: try logSomething(); try const num = mightError(); catch (e) { console.err(e); return -1; } const double = num * 2; return double;
The real solution to this is to make so that blocks are expressions. So we can do:
let double = try { maybeNumber() * 2 } catch { -1 }
Rust got it right
Yes, or errors should be values
this is the way
Why is rust so good at everything 😭
Was gonna say the same thing, but using Kotlin as an example.
Yep, in Rust everything just falls into place, it's just so well designed :D
Try scoping makes perfect sense. You're assuming that your catch block returns or throws. If it doesn't, then there's no guarantee that a value assigned in a try block isn't uninitialized. If your catch block didn't return in the first example, then your function would throw a TypeError because you can't multiply undefined by 2. Thus that line _can_ throw, and so belongs in the try block.
Then you can look at the return statement. Unless you want your function to be able to return undefined, then you have an error in your logic. To fix the logic, move the return statement into the try block with its dependencies so that it doesn't run if its dependencies didn't resolve.
that's why errors as values are just indefinitely better than exceptions and try/catch
also if you've watched to the end he discovered that and recognized the scoping makes sense how it is
JavaScript can't type error in multiplications, the only way you can error in a multiplication is if one of the factors's prototype's valueOf function throws
in the video's case, undefined * 2 is NaN
Scoping is necessary and correct. It is plain skill issues. The calculation based on the throwable function must be in the same scope. Putting it outside makes no sense. The design to return errors or throw them is equaliy good or bad. The problem in Javascript is the handling of the function definition (you may not know that a function throws). It could be changed inspired by swift, needing to add a throws section to the function definition if a function actually uses the throw statement.
This example tries using try-catch in errors-as-value manner. Just don't!
@@gabrielpeleskei3075 isn't that exactly what the comment said: "try scoping makes perfect sense"? what additional info does your comment give?
@@masterflitzer If you don't see any, that's good with me.
Theo must have something against pirates, pronouncing "var" like "vair."
verror, vare, vair . I don't car ☝️😃
I think it's because var is from variable.
I still hate it though
@@ccash3290 and that's why we should all say car as in carriage
@@567legodude I agree with your point and unfortunately that means we both need to pronounce "gif" as "jiff" now
Tomato tomato, potato potato 🤷 just the weigh she goes
6:20
Writes a custom error "Forcing var to never get hit" -> Sees the error in the logs -> Interesting that they have "Forcing var to never get hit" error 😂😂😂
I thought the same thing, Theo has the memory of a goldfish
try scope is annoying, but not a big deal.
It would be a strange break of consistency to have one particular block not be scoped. I'm looking at this from the perspective of C-like languages, where just using plain brackets alone serves no other purpose than to introduce scope.
You can use brackets on their own in javascript too. But I never had a situation where this made sense to me.
@@balduran It can make sense sometimes in C++ (probably Rust too) by allowing you to use block scope to control the lifetime of objects using RAII. In languages without RAII there is no reason to use it.
@@balduran I've tried to do it a couple of times, but it threw errors so I gave up, I'll have to revisit it sometime.
javascript devs discover in 2024 why try/catch statements create separate scope 🤣🤣🤣
it was designed in this way for Java and C++ for a very good reason. And most likely there was some small unknown language that had the same concept 10+ years before Java was even created
my exact thoughts, like dude that's not a js issue, it's an oop design thing. and as error handling goes, i'm loving how go/rust/zig treat errors as values, much pleasant to work with.
Js devs choose random hills to die on😅
The reason is memory. It always comes down memory.
@@xenitane this has next to nothing to do with oop design lol block scoping (with brackets) comes from C, same place Java and C++ got it from.
are you even following the discussion? it's more so about function scope vs. block scope, not scope in general. considering JS has embraced both design paradigms at some point it's not crazy to see some people weigh the pros/cons of both
My 5 year old son instantly said "Lest some scoped variables could be left in an invalid/indeterminate state!". He's so smart.
After my daughter's first day of school. She came home and declared "zero is the first number". I was happy she was on the right team. A week later she asked: "Dad are you sure that if you count long enough you will not get back to zero". I said "yes I'm sure". She wasn't buying it and asked me multiple times. At that point I tried to settle with fact that my daughter probably would become a C-developer.
@@05xpeter In fact you are not sure. There are at least half as many algebras in which counting upward would get one back to zero. This is equally real and possible in mathematics as the infinity you're indoctrinated with. Numbers aren't a physical thing, everything is a convention, and cyclic number systems are arguably even more rigorously defined than infinite sets, especially when it comes to integers.
@@milanstevic8424 Yeah there is always the possibility that see could become a mathematician. I guess I though about that when I named her "Tilde", my wife wan't happy when she found out that I had put a math easter egg in her name. But as I told her it had to be "Tilde" I couldn't call her "Line" (that's my wife's name) and "Cirkle" is a little to much.
blocked scope is the correct implementation. NNN
No nut November?
@@ononaokisama No Nut November
bro wrote a community note on a yt video
Watch to the end - Theo realizes the scoping in `try {}` is needed after all! I hope Theo pins a comment about this.
The problem: while declarations are hoisted in blocks, initializations are not. An error within a try can result in an initialization not being reached; therefore it is control flow.
An additional argument is syntax: when something looks like a block (surrounded by the curly brackets) why shouldn't it be a block? Any language I've used in my decades of coding handles it this way and it makes perfect sense.
The idea would be changing the syntax along the scoping behavior, to prevent that consistency issue.
Object literals use curly braces, but they're not a block per se. But I guess it's a bit different when it's an expression rather than a statement, which is usually easy to distinguish
This is why I've gotten into the habit of moving try-catch blocks into their own functions and use something like Go's pattern (which I really like).
This is the correct answer. I took the same pattern from Elixir, where try blocks have a return and their result can be assigned, making them effectively tiny functions with syntax sugar.
That'd one extra reason to love golang's errors as values implementation... Everything is so crystal clear because of that! (that and the fact that Go has even less syntactic sugar than C 🤣)
Theo: "var hoisting behavior is bad"
Also Theo: "we should be able to hoist variables outside of try blocks"
Makes sense.
js mind virus take from theo again xD
@@ninocraft1 isn't he a React shill
In his defense, I would say that hoisting within a try block is very different than from within a function or a loop.
@@akam9919 Except that it's completely inconsistent with every other block scoping behavior in JS. Why would you make one single exception? That is setting yourself up for disaster.
As someone in the process of learning js, the fact that var hoists *differently* is what makes me feel a little hysterical.
This is exactly where I prefer the approach that Kotlin takes. It maintains the scoping but allows returns from the evaluated value of the try/catch flow. It would look a bit like the following in JS:
const num = try {
getNumber();
} catch (e) {
console.log(e);
return -1
}
I've liked Option/Result type of error handling. Especially with exhaustive pattern matching languages. Code is much more transparent on what it can return, increased type safety, and compiler can reason deeper about your code and catch more errors at compile time. Also gives you the lower level primitives to build custom higher level error handling more suited to your particular use case. Having to use `let` outside of the try/catch as a workaround and not being able to use `const` just feels dirty and unnecessarily imperative to me.
I feel like we would all be better programmers if we are forced to deal with error cases, enforced by the types, more often. It would be annoying, and maybe less efficient, but hopefully less breaking stuff :)
@@fjjfjfj Performance is an interesting point. There are probably scenarios where checks can be skipped. There's also scenarios where things like bounds checking of arrays don't need to be performed because the compiler can guarantee it's not empty. There's even ways to create types for non-empty arrays as well. I don't think we're there yet, but I've always wondering if compiler technology improves and we can specify our intent at higher levels, the compilers can make more assumptions about the code and optimize better. Really hard to do with imperative / procedural code though.
12 minutes of straight yap
"What's control flow?.." - Theo, Aug 2024 :)
The main shortcoming of the try catch syntax is that it isn't an expression. In practice it forces the pattern of isolating the block in another function so that one can return something useful in both try and catch blocks. e.g. try { return getUnsafeStuff() } catch (err) { return getAlternativeStuff() }
A new expression syntax would help. But I've people using a helper like this: const result = orElse(() => getUnsafeStuff(), err => getAlternativeStuff())
This is why people like errors as values. It's not that returning an error is much different than throwing it, but the fact that you only scope the error case outside of the function is what people like.
The fact that a substantial number of people think that curly braces should sometimes create a new scope and sometimes not is honestly kind of insane to me. The reason var works is not because it hoists but because of inconsistent scoping rules like this. The hoisting part has nothing to do with.
Another reason to love Rust with match assignment destructuring and Result monad. Dealing with failable code while assigning something is a piece of cake.
@6:23 Theo finds his own error message interesting 😂
Love these javascript vids. Catharsis all wrapped in a senseless block.
You should do one on defining objects with variables for keys. That syntax is so confusing where the key can be dropped or wrapped in brackets, or spread, but only shallowly spread... And there's no built-in optimised deep copy! I have to trust a util or write a case for every symbol, collection, set, array, etc, etc if I plan to reuse it lol
structuredClone() is a built-in deep-clone function if I understood your problem correctly
@paxdriver lodash solves deep copy (and a bunch of other utilities that should be be built in). big time saver.
Great to see you learnt why its a good idea to scope the try block.
If the block scope of the function leaks out of the try then throwing midway through would result in uninitialized variables. With using let outside and reassigning you have an explicit initialization which can be set to a new value if the try succeeds. If you hoist the assignments out you either need var semantics where everything is initialized to undefined at the start of the function and then changed wherever the var statement occurs.
Uninitialized state is very unsafe, especially with block-scoped variables as they are initialized at statement position not hoisted to the top of the block, so variable initialization might not be reached and then subsequent code which assumed initialization happened could corrupt memory and crash the process.
Unscoped try/catch wouldn’t work the way you think it should.
I think it's also worth covering that it could also break on the line that does the assignment, but still *before* assignment, depending on what it does. If `const foo = MightThrow()` then `MightThrow()` throwing would occur before assignment as function execution takes place before assignment. I.e. don't think of it as every separate line potentially failing so much as every separate operation within every line.
I wish typescript could tell me if something can throw. The amount of things you don’t know can throw is so hard to deal with
This is actually something I've searched about as I thought the very same thing as you. And luckily, Anders Hejlsberg (C# & TS creator) has talked about it.
His opinion was that although beneficial for your code, integrating this with 3rd party libraries would be really bad DX as you would have to handle a bunch of cases that you may not even be interested about from these libraries.
Of course his opinion was much deeper than that, but that's the overall opinion I got from him.
I don't wanna link anything here as I don't know how youtube treats comments with links (bots are allowed but actual people not, huh?). But you can look it up as "The Trouble with Checked Exceptions
A Conversation with Anders Hejlsberg, Part II" it's an article from "Artima" where they interview uncle Anders.
It's a good and recommended 5min read :)
I wished that once too, and then I tried java which statically analyses exceptions. It's very annoying to deal with. I think its just better to have one catch-all statement to handle all potential runtime exceptions. (But if if it was an optional feature of typescript I would still like it for some use cases)
@@panmateusz It's a little more complicated in Java. Initially Java used so-called checked exceptions and handling was mandatory. Either by try/catch or using the 'throws' keyword after a method signature with all exceptions this method could throw. Well, that decision didn't age very well and runtime exceptions were introduced. They work basically like JS errors. Older parts of the stdlib and some libraries still use checked exceptions, more modern parts and most libraries use runtime exceptions. This is also very annoying. I'm really thankful that JetBrains dropped checked exceptions in Kotlin completly. There's an annotation available instead of the keyword 'throws', but it's not mandatory to use it.
But Kotlin allows you to not use exceptions at all. Thanks to sealed classes and some other language features, it's very easy to implement a result object for handling success and error states (Kotlin has a native result class, but IMO it's not that great, so I wrote my own). It's not as convenient as with Rust, but it works fine, makes error handling easier and forces you to handle errors. It's a little verbose at times, sometimes annoying, but in the end, a way better error handling with less potential for bugs. And at least for my taste, nicer than Go's approach will tuples and the omni-present null-checks of err.
You should just assume that every non-trivial function can throw (this is usually true).
With a lot of people doggedly holding on to their various opinions, it's honestly good to see someone change their opinion while working through something. Honestly based.
I don't know why this wasn't brought up, but the solution is absolutely not to have the variable be hoisted, the solution is to have try (and if and other control flow) as an expression.
const num = try { getNumber() } catch (e) { return -1; };
const double = num * 2;
Rust does this and it's excellent.
I don't even think this would interfere with the current JS grammar since try as an expression is illegal, and the return of the last value of a try statement is a noop so equivalent to the old behaviour.
If err != nil
worse
@@theairaccumulator7144 you are worse, this is better
@@theairaccumulator7144 I don't think it's worse, atleast it doesn't indent the code for no reason. But I agree it might get more chatty if you need to handle several errors.
its annoying but useful
@@steadexe You should place as little as possible inside your try-catch. If your entire code is wrapped in a try-catch then that's just a skill issue. The indentation should be pretty much the same as what "if err != nil" causes.
In fact, wrapping as little as possible in try-catch is preferable since if you have a lot of your code wrapped in it, you might catch unexpected and unintended errors which could be problematic in many ways. You also want to handle errors as quickly as possible and restore to a normal state, or exit with an expected failure state rather than an unknown one.
I mean, anything that might depend on unreliable (read: may fail/error) code is also, by definition, unreliable. A chain is only a strong as its weakest link. Therefore, it makes more sense to put the lines that may throw, and everything else that depends on it inside the try block, especially since in this simple case it just conditionally doubles the value and returns it with no extra dependencies. This solves the problem of trying to access an out-of-scope variable, and also keeps the code better organized, and easier to read and maintain.
Keep in mind, I/O failure is also a thing. Your file may succesfully open, but that doesn't necessarily mean it'll succesfully read, or write. Might as well group all operations involving the same unreliable foreign resource into a single try-catch block.
Also, var hoisting is a mistake and should be violently ignored moving forward. It's only kept around for backwards compatibility with old, broken code. Please, just pretend it doesn't exist. The only circumstance one could possibly prefer var hoisting is to use it instead of the finally block, to check-not-undefined and then free resources. But it's ridiculous, no sane person would prefer to declare, but not define a variable at the top of the scope, IMPLICITLY. And if you think you found a legitimate usecase, please just study more. There's a reason why this behavior does not exist in other languages, and if you want to declare, but not define a variable at the top of the scope, you have to do so explicitly.
If it wasn't because mistakes were made and old JS must still continue working, let would've replaced var.
javascript devs are too comfortable with changing javascript than themselves
A bug: skill issue
Js devs: it must be the language, no it is the browser, no it is the device 💀😅
@@warrenarnoldmusic Nah, it's mostly the rust devs who try out javascript and don't understand a thing.
Hot take: that's how things probably should be, any part of computer system should be able to evolve through change (except for null being an object, obviously, that should stay forever this way). Javascript was mediocre before it become good.
@DarthVader11912 yeah right? blame it on rust dev
A bug: skill issue
Js devs: it must be the language, no it is the browser, no it is the device 💀😅
it's always the hardware's fault!
I'm not sure why this wasn't mentioned, but `finally` helps a lot when writing `try/catch` in JS. There are a lot of control flow issues that you can solve there. It allows you to write some code after that will execute even if the failure happens.
It's a bad idea to have control flow statements inside the `finally` block.
It should only be used for cleanup code, ex: Closing files, DBs etc.
@@captain.america1123I would argue that since finally will execute regardless of what happens with the try/catch, it *can* be a perfect place to self-contain a proper response
But the use-case of a finally block is already quite situational in my opinion, and more suited to the tasks you mentioned (db transactions, file handling closure, removing global variable pollutants, etc)
So can != should, but programmers gonna program
I definitely agree that it’s not something that should be used _all_ of the time. In retrospect, my original comment shouldn’t have called out the “control flow” piece specifically. I only meant to draw attention to the third part of “try/catch” that isn’t discussed much which can be used to solve _some_ issues and help with writing cleaner code.
I just pull those kinds of short try-catch variable definitions into their own function and suddenly it's no longer a problem. It can even be a local function within the function that needs to do it.
Nice! Thanks for openly showing that changing your mind after learning something is a good thing :D
We have to choose between consistent block scopes or marginally better ergonomics here. I’m happy the authors chose consistency.
lol I loved the change of mind when you figure it out. Honestly, you can get away with a lot of things mixing the `try/catch` syntax and the `.catch()` syntax when necessary and it's really ok. What bothers me the most with exceptions is that they are not typed. In java, we declare a function saying what the function can throw so the catch block is also typed. This is not the case with Typescript and I believe it sucks
I am glad you changed your mind, because it makes 100 percent sense.
i love the realization mid vid, and acceptance and opinion change. well done
I've read about hoisting and understood it and then almost completely forgot about it for so many times in the past 8 years that I can't even keep count anymore.
I started pulling my hair out thirty seconds into this video but by the end was so fucking happy. Not just that you flipped sides (although as you say the DX does suck) but because it takes a distinct absence of ego to call BS on your own position after spending nearly ten minutes explaining it in public. If only more debates and conversations were had in good faith like this. Alright I've been watching you for like two years ago but because of this particular one, yeah okay, I'm finally clicking the fucking sub button.
That hair and mustache look like something Weird Al would do as a parody of a failed 70s porn actor.
The problem is that hoisting shouldn't be an option. That makes no sense. Scopes should always be real and have no edge cases but that's JS for you.
I like it the way they do it in Kotlin. Where you can return a value from the try/catch and assign that to the variable you want.
Respect for beeing honest about realizing why it is this way. Also very important as a dev to be honest about beeing wrong
In JS you need to think of try-catch as something that protects a whole scope, like the whole function. Happy path should happen in try{} and Failure path should happen in catch{}. If something always needs to happen last it must happen in finally{}
Your 2-space-width tabs make you my sworn enemy.
This makes sense if you take the return out of the catch. Anything which is then hoisted could be an error outside of the try catch. Accessing the variable again would mean that you are now referencing the error object
i'm not functional programming this is a topic as old as time. basically an Either monad that can be passed from one function to the next.
Every day, I miss Elixir where a try statement may have a return value. Solves the issue of graceful fallback
Try catch behaves like this in all Java-like languages. The whole try-catch pattern is rotten and it's not JavaScript's fault.
This is why packages like Effect are awesome.
This should not take a toy example and this amount of soul-searching from any dev with a modicum of experience.
Having the try/catch statements both be something that is considered their own code blocks is honestly something I agree with. Try only runs code up until an error is thrown, in which case it then runs the catch code, so if you define a variable within a try block but it crashes before it reaches this definition, then using this undefined variable outside the try statement wouldn't be possible. I also like the brackets counting for consistency, where every block created with {} is its own block. It would be really strange if try was the only block with brackets that didnt create a scope, and since it creates scope it prevents the accessing of a variable that doesn't exist.
I think try scoping makes total sense. The actual solution to the ergonomic issues created by that aren't solved by "removing" the scoping, but one option would be to make "try/catch" an expression (i.e. it returns a value, can be assigned to a variable). Something like this:
const num = try {
getNumber()
} catch (e) {
console.log(e);
-1
}
This is the way Kotlin works, and makes code very nice and readable. However, I'm not sure if this syntax is really fit for JS to begin with (the implicit return), so there's that.
Bonus: it's also very TypeScript friendly, as it can automatically make a union type out of the return values for each of try and catch blocks (or `T | undefined` if one of them doesn't return).
3:07 I don't get what is wrong with having all your code inside the try block.
and what is wrong with if-else to handle errors or "unexpected"/not happy paths conditions
Yeah I don’t see why it’s bad either. If you want to handle errors from the other function call then why not put a separate try/catch in that other function?
The problem it separates the error handling from the point where the error arises. As the function becomes longer, this problem grows. The solution is typically to factor out the part of the function that can fail into a new function that includes the error handling.
There's other languages that handle this in a very unique way, that I think would be doable in typescript. For example Scala's Try
Basically it's like a try catch block, but without catch. The catch happens in the type definition. So any exception thrown is saved in the return type. The simplest example is a function to convert a string to number:
def parseInt(s: String): Try[Int] = Try(s.toInt)
Now the result of the function parseInt is of type Try, which already tells you (much more explicitly than the ghostly typescript errors) that this number that you get returned is really inside this Try wrapper which can fail, which forces you to deal with the error before accessing it, much like Go with their err != nil spam. Just that in Scala, you can decide when to unwrap and deal with the error, instead of doing it right away.
With TypeScript having such a strong type system, I'm surprised that it doesn't have anything like this for error handling
JS does that because it create a new scope whenever it encounter { }, you can even do something like
main:{
// something somethig
}
and it will be an entire new space there.
of course the facking var will escape it.
Agree on principle but would be confusing that this block differs from other blocks. Value in being consistent.
PS. Did not know about "var-hoisting" :D
Imagine having all of your variables not being destroyed after the try block.
It's good that you came around to the correct way of thinking that this way of it working makes sense *and* that JavaScript sucks. Another example for you, consider a normal block in a language like C: int main ( void ) { int a = foo(); int b = bar(); { int c = a + b; } printf( "%d
", c ); return 0; } // will not work because `c` had scope only within that inner block, but would if the printf() call was made inside that inner block. This is an overly simplistic example, but it demonstrates the point all the same that a variable can leave an inner scope.
It's imo good dx that blocks work consistently. You know what you are getting, regardless whether you got an if, try, function or => before those { }
Also a TH-camr saying "x% of my viewers aren't subscribed" is an automatic unsubscribe
In Python a try block can have an else block, which only runs if there was no exception thrown. With proper scoping that would make even more sense than in Python. So you could do:
try {
const x = foo();
} catch (e) {
// cannot use x
} else {
bar(x);
} finally {
// cannot use x
}
// cannot use x
I kinda see what you mean, but I'd argue this makes even less sense in Python and is in fact one of the reasons that:
1 - variables are not declared;
2 - not every block suite creates a new scope.
What I mean is that the confusion on what's defined were in the control flow in general is something to avoid as a design principle, which leads to the above two points.
Anyway, I keep trying to give JS a chance, but...
aaannnd this is why i kinda dislike python... an esoteric block of code that can only run if there was no exception and conceptually merges if and try blocks? shouldn't the code that only runs if an exception wasn't thrown be just the rest of the function?
just pre-set your variables outside the scope when it applies people, is not the end of the world
Another solution would be a try expression where the try block can return a value (or return from the function). Then you can declare the variable to be set as const.
what the fuck
@@Lambda_Ovineesoteric block of code is kinda funny ngl. There are else statements for both for and while loops in python though, so having it for try is predictable
On the other hand, "every code block is a contained scope" is good in its simplicity. It'd be nice if there were some alternative that doesn't require a closed block though
Bruh 10:05, if you know try block is not going to fail then keep things in the try block, wth on about there. Also it is not that try block is a control flow in and of itself but anything is a scope when you put them inside {}. You can create scope with {} without using any keyword, you know that right?
Explicitly defining the variable outside makes it obvious that it needs to be handled to people coming in later. The better answer is the ability to use try as an expression.
Theoretically no scope could make sense in a different operator where catch guarantees you leave the function.
Honestly I see no issues here, thank God theo changed his mind.
I spend most of my time coding with Python where you'd catch a named exception depending of the event that caused the exception, the only reason why I see this as an issue is where you have deeply nested throw statements when your function is calling another function that calls a function that throws an exception. Thinking about it makes me nervous.
😂
Glad you came around. I was going to be disappointed.
What we need is post-initializable constants in JS and TypeScript should be able to handle this in the type system to ensure no funky behaviour.
This is an argument i heard a while back, where the guy was pushing 'await' with .catch notation.
One of the things I have a problem with when it comes to JavaScript is that you often have absolutely no clue which functions may throw. It's crazy.
Applying normal scoping to a try block encourages you to put code in the try that doesn't relate to the error condition that the catch is intended to deal with, but which needs to access values computed in the try. I prefer for try blocks to be as small as possible, and only cover the code which the catch deals with, but that then complicates getting data out of the try block because of the scoping rules.
My workaround is wrapping the try/catch in an iife, so that I can just put the return value of the fallible call in a const and get on with my life
The problem is not with the try/catch but with the entire concept of throwing errors.
This video is a whole ass skill issue.
This is one hell of a rubber ducking with 6.8k people...
I thought it's default mindset that anything in any { } should create a scope
I think anyone who hates JS should go try to write their own language. I did and it made me realize how elegant JS is in its simplicity and orthogonal core concepts.
Also you come to intuitively understand all kinds of low-level things like why try-catch is block-scoped, just like Theo did in this video.
Ah ! Can't agree more. Always annoying to make your let X outside the try rather than just making it a const.
When I heard your intro claim to be a "spicy" take I was like how spicy can a take be on try/catch? I guess not all heroes wear capes! But then you explained and yeah the scoping on try catch has been a huge annoyance! Not sure why that would be an unpopular opinion though. Will the pro-try/catch scoping mob come after you with pitchforks? lol.
You can fix that by allowing to return from try into a variable. Rust's do that, you can assign any block to a variable
Honestly, I believe the way it's setup is correct (given how JS works). If a for-loop has the same behaviors, why should try-catch be an exception? If the first baseman catches a ball thrown by the pitcher before you get there, you're OUT. You don't get to keep going all the way to the umpire/home plate - you're no longer active or assigned any role or position until you get called to bat again.
If you're trying to define variables inside a try-catch but use them outside of it, then at least figure out why you're doing that instead of arguing about the implementation no different from other scopes.
Obviously what I've said does not apply to other languages that were designed with these things in mind, but since JS already has a definition of a "scope" then it only makes sense to follow the standard, not create an entirely new one to please a few people.
just for the sake of clarity around the 3:00 mark: obviously, moving `const num` into the `try {}` block while leaving `const double` outside of said block will break, because that's how ES6 and the "block-scoped" `let` and `const` were designed to work. these did not replace `var` as everybody loves to tout.
`try`/`catch` was around long before ES6, if you use `var num` (which is not block-scoped) inside the `try { }` block, then `num` is accessible outside of the `try`/`catch`, and you won't get a "ReferenceError: num is not defined"...
this is only an issue today because we have different "building blocks" to play with, blocks that the designers of the other, older "blocks" did not foresee. my point is: when you use `var`, then `try`/`catch` makes sense. if you use `let` and/or `const`, then it does not.
but wait... it doesn't "hoist `var` out" of the block, `var` is simply function-scoped, not block-scoped.
Rust handles this so nicely. But in js i would love a try catch that looked like a ternary where the second value is a function with an exception argument. If the first returns a value, the second must also. That way variables still end up getting set.
Hmm for me it actually makes perfect sense, it is expected part of the try clausule is not executed(completely) so variable declarations can not be assumed after the try clausule.
Like a lot of things, having try { } catch { } be an expression would solve this problem.
This is the exact reason why they made var hoist variables. Beginners declaring variables within if blocks and wondering why they cannot access the variable outside the if block.
I think the better solution to using an uninitialized variable is for it to be an error to use a variable whose var statement hasn't been reached, just like it is for a let or const.
There's a reason try{} is a scope for declarations in pretty much every language that has try{}.
Just put each and every potentially undefined handle (database connection or rather a grab from some connection pool etc.) inside dedicated and properly scoped semantic codeblock, no?
Try-catch in JS is a mess for many reasons. My favorite pain points are scoping, as mentioned, as well as having to do if instanceof checks for different types of errors, and possibly worst of all not knowing which errors are potentially thrown at any given time. Considering you can do like in C++ and throw literally any value, it makes it very difficult to catch the errors gracefully because `error` will always be `any` type. It's just a lot of boilerplate to deal with.
Not a dumb thing. The fact that you do not realize that this behavior is by design is what truly sucks.
The "only" reason is exactly why it should be the way it is.
I use this HOC a lot because of how ugly try/catch is:
const safe = (fn, valueOnError) => (…args) => {
try {
return [fn(…args), null];
} catch(e) {
return [valueOnError, e];
}
}
I'd like to be able to catch specific exception types instead of everything being Error, TS improves this a bit
Yes, but no. while try scoping can be painful to use in some or many cases, it would be more confusing to have blocks (code between curly braces) act differently when it's try and normally everywhere else
If there was some keyword other than var for this, once people were familiar, I'm sure it would be fine.
For compiled languages, I've also seen the concept of flow scope, where the variable can still be accessed if nothing can potentially cause it to be uninitialized.
It should be changed to use the try statement where an error will occur, and have a catch block, that must return, follow that try statement. That way you're forced to handle the error if you need a value set:
try logSomething();
try const num = mightError();
catch (e) {
console.err(e);
return -1;
}
const double = num * 2;
return double;
Would be nice if catch received the try block context as an arg.