This might fix error handling in JS
JavaScript error handling is a mess, and while there's a promising proposal for a safe assignment operator, its chances of becoming a reality are slim.
Errors in JavaScript and TypeScript kind of suck. I don't think that's a hot take; generally speaking, even the most die-hard TypeScript fans can admit that try-catch is far from ideal. This method of dealing with errors in our applications is rough. It's so rough that the way we build things, like uploading files, requires using an entirely different library for managing our runtime with Effect TS. As cool as Effect is, it would be a lot cooler if the syntax of JavaScript just let us safely call something that could error and ideally could even give us that error as an actual value back that we can use for things.
You know, if you know me at all, you know I don't like to give Go credit for stuff, but it's one of the few things they did right. The fact that your error comes back as a value that you check before you proceed is not a bad thing. In JavaScript, we have to wrap our code with a try-catch block, and we can't bind variables how you'd expect. It's just kind of a mess, and that sucks. That's why someone wrote a proposal for a better way to handle errors.
We’re going to take a look at this proposal and, importantly, why I don't think it can ship, but also what we can do instead. This proposal is by a community member, Arthur. He's really chill and works on K and a bunch of other cool things. He made this official TC39 proposal for a safe assignment operator. The goal is to use this question mark equals syntax when you call await on something async, and it gives you back a tuple with the error as well as the actual thing that it responded with.
I mean, this looks really good, right? As soon as this was seen, it went viral. If we look at Kent's tweet, it has 1.5k likes on a JavaScript syntax thing. Matt made a really important call-out here that it's a stage zero proposal. The heat death of the universe is also incoming; the syntax is not confirmed. I've seen a couple of people, even other YouTubers, covering it as if this is guaranteed to happen. However, there are things like stage 2.5 plus that might never ship. This has not been acknowledged even by the official standards committee, and the likelihood that this ships is effectively zero.
There’s an important reason why, and we'll get to that in a bit. Though, I want to dig into what this is and how it works. In fact, why don't we go ahead and write this ourselves? I think that'll be a fun challenge. Initially, we're going to write this in a way that's not super type-safe, but it will work.
Let's define a function called safeAwait. It needs to have a type; this takes in a promise which returns type T. Now we can do the try-catch here. We’ll say const result = await promise; return null; If the result is not successful, we catch the error e and return e, null. This can be an async function.
Now, let's create a function called random. It will generate a random number using Math.random(). If the random number is greater than 0.5, we throw a new error with the message "value too high"; otherwise, we return "success". Now you have this function that is returning a promise of a string, but it can throw an error half the time. If we were to just await that inside of something, there's a high chance it's going to throw.
But if we use safeAwait, we can do this: const [error, result] = await safeAwait(random());. Now we have error and result. The types are wrong; I have to await that. Oh, it's mad with top levels. Yeah, I love JavaScript! So now we have the error, which is unknown because we don't really assign it a type. The result is also unknown, but this worked. We now have the error if this failed and the result if it didn't. We just don't have the types working, but I think it's pretty cool that JavaScript is flexible enough that if you want to write your own equivalent like this, you can.
You can't have the syntax with the question mark here, but these are effectively doing the same thing. The safeAwait function is providing the same functionality that this proposal would include. You could even write a Babel Plugin or a TypeScript hack that will let you write this code, and it would compile out to be the thing that I just wrote there.
We can actually do better; we can make this properly type-safe. This just involves some custom return types. First, we have to give it an error type, and we also have to give it a return type to be more strict than TypeScript is by default with the inference. What we've done here is pretty neat. If we want to keep the error first, then it would be null, T or...
Type safety in TypeScript can be enhanced with custom return types and error handling, making your code cleaner and more reliable without sacrificing functionality.
In discussing the syntax and functionality of error handling in TypeScript, it is important to note that the safe await function is providing the same functionality that this proposal would include. Although you can't have the syntax with the question mark here, these functions effectively achieve similar results. You could even write a Babel Plugin or a TypeScript hack that would allow you to write this code, which would then compile to the equivalent of what I just described.
We can actually do better by making this process properly type-safe. This just involves some custom return types. First, we have to give it an error type, and we also need to specify a return type to be more strict than TypeScript is by default with its inference. What we have done here is to ensure that if we want to keep the error first, we would have null | T | E. With a little bit of type hacking, specifically by adding a return type to be more strict and incorporating a generic error, we can theoretically pass custom error types if desired.
Now we have a very simple function that will give us the right types: we have an error which could be Error | null and a result which could be string | null. If we encounter an error, we return early, which allows TypeScript to understand that the result is a string. Before we check that, TypeScript might think the result is string | null because it doesn't know yet until we've parsed the error. However, now we have the full Golang experience with a simple function, which I think is pretty cool.
Interestingly, I'm not the only one who has thought about this. There is a library called never throw that Matt showcased. This library includes generator functions, which makes the process even more engaging. The function could either error out or pass successfully, and when you call it with the safe try helper, you receive the result type. The result type is very similar to what I was showing earlier, where the safe try function returns a type that does not throw errors. This function is smart enough to parse it because it is a generator.
The safe try feature in never throw is particularly impressive as it allows you to write code in a conventional manner, focusing on the happy path while acting as if it won't error. Unlike traditional code, safe try keeps track of all the errors that might occur and allows you to handle them later on. If you wrap something that can throw with this inside a function, you can consume that somewhere else, which enables the error case to be hoisted all the way up using the types.
However, there are some catches: you have to yield instead of just returning traditionally. I have mixed feelings about generators, but they are indeed making a comeback. If you use yield and safe unwrap, you gain direct access to the type without needing to check for errors, and it will hoist the error for you. The result of safe try knows that it can view multiple kinds of errors that could be yielded or thrown when it fails, such as a JSON parse error or a local storage error.
While this seems like a very small subset of what you might get with something like effect, it is really easy to consume and looks great. Personally, I haven't tried never throw yet, but it appears promising. If you prefer not to adopt generators and all that complexity, something like what I wrote with the safe await function is super easy to adopt.
In summary, we have covered what it would look like to build this functionality and why you would want it. However, there is one last piece we must address: why this can never ship. David Blast is one of those few individuals whose understanding of TypeScript is so profound that if we disagree, there is a near 100% chance he is right and I am wrong. He has a deep understanding of TypeScript, which is almost unparalleled. You might have seen my videos about his work before; he is the one who created The TypeScript Benchmark, which measures the performance of the TypeScript server in interpreting your types. David understands TypeScript so well that he can audit your codebase and make small changes that enhance VS Code's IntelliSense and language features.
Trust the expert; when in doubt, listen to the one who knows best.
The safe await function is super easy to adopt. We have covered what it would look like to build this and why you would want it. However, there is one last piece we have to cover, which is why this can never ship. David Blast is one of those few people that, if we disagree, there’s a near 100% chance he is right and I am wrong. He deeply understands TypeScript on a level almost no one does. You might have seen my videos about his work before; he’s the one who made the TypeScript Benchmark—not just benchmarking the runtime, but benchmarking the performance of the TypeScript server in interpreting your types.
He understands the way TypeScript works so well that he can audit your codebase and make small changes that prevent VS Code’s IntelliSense and language server from crashing. He is truly exceptional. I’ve accidentally summoned David in my Twitter replies a few times, and he was right each time. I would not want to be on the side disagreeing with him because it means that I am wrong. Thankfully, over time, I’ve slowly become more and more aligned with him because he’s always right.
When he saw this syntax proposed, he replied that it will torpedo if they don’t change the syntax soon. It is too much like question mark question mark equals and breaks intuition about the question mark relating to null or undefined. This is really important because this syntax lets you assign a value if it’s not already assigned. For example, if you have something like let x: string; and if Math.random() is greater than 0.5, then x equals "hello." However, if that case didn’t get hit, you want to assign it to something else.
We could put this in a function or have a check to see if x already exists or not. Now, we will only reassign x if it doesn’t already have a truthy value. Actually, I think it has to be null or undefined for this to work. This is a really nice syntax where this only gets assigned if it is null, and it gets skipped otherwise. Deleting one of these question marks is just a massive confusion. Now that he has proposed it like that and mentioned that part, he is right.
Gabriel just confirmed for me that this only works if it’s null or undefined. If this was set to zero or a nullish value, even an empty string, this reassignment won’t work unless it’s set to null or undefined. This is a really handy syntax for turning a complex machine of assignments into a simple way of assigning the value. Like most of you, and I know I’m included here, what I would have done is if x equals defined, then x equals it was skipped.
Why is this important? Because I haven’t assigned it, it’ll even be mad about that. God, JavaScript! We skipped the random check. This is a really handy syntax, and I should probably use it more; it makes life a lot easier in many places. The knowledge call essence is great. The double question mark syntax does so many useful things; it should not be tainted. The question mark almost always means an if of some form. The single question mark is for ternaries, while the double question mark is for coalescence. These things are great, but because of that, this is stepping on existing mental model space.
The types of errors are an annoying thing to deal with. The library that Matt showed off has error types built into it, but it would be a lot nicer if they were built into TypeScript itself. The fact that you can infer types for returns but not for errors is obnoxious. There aren’t really good reasons to not have a type system on top of the errors, and it sucks. This is a proposal; apparently, there’s a different one as well underneath. The throws keyword would be used to indicate that a function can throw a specific error type. This way, you can specify the errors a given function is capable of throwing, so when you catch it, you know which errors could exist.
However, this brings up a big old mess with the throws clause. The problem is if you make a function fn that takes in a number and throws if you pass it zero, it’s not clear that when you use it, it might throw an error. It is also not clear what the type of the error is going to be, even if you do know it’s going to throw an error. These are both very real problems. By introducing optional checked exceptions, type systems can utilize them for exception handling. However, checked exceptions aren’t agreed upon—for example, Anders Hejlsberg disagrees with it. There’s an interesting little article titled The Trouble with Checked Exceptions that discusses this further.
Understanding exceptions in JavaScript is like navigating a maze—it's tricky, unpredictable, and often leaves you guessing what might come next.
In programming, it is essential to specify the errors a given function is capable of throwing. This way, when you catch an error, it knows which errors could exist. However, the current situation can be described as a big old mess. For example, if you create a function, FN, that takes in a number and throws an error when it receives zero, it is not clear to the user that this function might throw an error or what the type of that error will be. This ambiguity presents two very real problems.
To address these issues, the introduction of optional checked exceptions that type systems can utilize for exception handling has been proposed. However, the concept of checked exceptions is not universally agreed upon. For instance, Anders Hejlsberg has expressed disagreement with this approach, describing it as handcuffs. In an interesting article titled The Trouble with Checked Exceptions: A Conversation with Anders, he discusses how exceptions are hard to check because you cannot know all the things that could cause an exception. Although I haven't read this article in detail, I can offer a deeper dive into exceptions and errors, particularly from the perspective of the creator of both C# and TypeScript, if there is interest.
In our discussion, we have a function FN that throws a string, similar to the syntax I mentioned earlier. Here, we can specify a return type for the function that indicates it can throw an error. The function returns a promise that resolves to a string, but it also has an error that it throws. Daniel G, who is the project manager for the TypeScript team, is actively involved in ensuring that these developments are successful. One of the ideas being considered is not to force users to catch exceptions but rather to improve the inference of types for catch clause variables.
Ryan, who is a key member of the TypeScript team with a deep understanding of these issues, provided a lengthy comment. After reviewing all the comments over the years and engaging in much internal discussion, the consensus is that the JavaScript runtime and overall ecosystem do not provide a suitable platform to build this feature in a way that meets user expectations. JavaScript does not treat errors as first-class citizens adequately enough to implement this feature effectively. It is too likely that you will encounter an error that does not conform to the expected shape.
To clarify the situation, we are opting to close the issue rather than add the feature. Similar to the approach taken with minification, we are implementing a two-week cooldown period on further comments. There are several different facets implied by the proposal, and it is worth breaking them down individually. The first facet is the ability for a function to describe what kinds of exceptions it throws, which would have corresponding effects on catch clause variables, akin to typed exceptions. Additionally, there is the ability to enforce that certain exceptions are explicitly handled or declared as rethrown, also known as checked exceptions.
I can understand why implementing this in JavaScript would be challenging. While it may seem straightforward to add, the reality is that when writing code, it is difficult to know what types of errors you might encounter. This leads to different behaviors based on the types of exceptions thrown. Overall, we need to examine how exceptions are used in JavaScript today to see how they align with our goals of typing idiomatic JavaScript.
There are indeed scenarios in JavaScript where probing the thrown exception is useful. For instance, you might have code that is likely to throw a few known exceptions. In such cases, if the error is an instance of a type error, you might log it; if it is a string, you could convert it to uppercase; otherwise, you would just rethrow it. However, there are usually very few guarantees about what kind of values an error might actually have. The existing dynamic type test pattern used in TypeScript is appropriate for writing safe code, and more on that will be discussed later.
It is fair to say that you can almost never know what an error will be when dealing with JavaScript code. As CJ pointed out, it is technically impossible to predict what JavaScript code will throw. While one could argue that the same uncertainty applies to return values, there is slightly more predictability in that regard. A proposed TC39 feature, pattern matching in catch clauses, could make these types of checks more ergonomic while addressing these challenges.
In JavaScript, error handling is often a guessing game, and the lack of clear documentation on exceptions makes it even harder to navigate.
In the realm of JavaScript, there are extremely few guarantees regarding the values that an error, denoted as e, might actually possess. The existing Dynamic type test pattern used in TypeScript is deemed appropriate for writing safe code, but it raises concerns about the unpredictability of what e might throw. As previously mentioned, one can never truly know what JavaScript code is going to throw. This uncertainty extends to whether the code is calling browser functions or other elements, making e as unpredictable as it gets. As Chad pointed out, this situation is technically impossible to navigate effectively.
The discussion leads to the proposed TC39 feature of pattern matching in catch clauses, which could enhance the ergonomics of these checks while providing useful runtime guarantees. This concept intrigues me, especially given my affinity for pattern matching. The idea of having multiple catches with checks is indeed appealing, and it resonates well with my functional programming mindset.
However, when examining the landscape of JavaScript libraries, it becomes evident that the rich inheritance hierarchies of various error exception classes, commonly seen in languages like C and Java, are not widely adopted in the JavaScript ecosystem. This situation is putting it lightly; in fact, for most libraries I use, I often find myself unaware of where and how they handle errors. For instance, while Low Dash boasts 200 pages of documentation, it lacks any description of the types of exceptions that may be thrown, despite the source code indicating that several functions can indeed throw exceptions.
Moreover, the one apparent user-surfable throw in jQuery is not mentioned in the documentation at all. React does mention some exceptions it can throw, but it does so vaguely, opting for language like "throws an error" without specifying the types of exceptions. A comprehensive 850-page book on Material UI fails to mention exceptions entirely, only discussing throws from user code. Similarly, there are no documented exceptions in XState, and the Spelt documentation, spanning 100 pages, merely states that it "throws an error" in one instance.
The Node documentation does not allow for accurate predictions regarding which properties will be present in a failing call, such as fs.open. This lack of clarity is a significant issue; while documentation is a great starting point, it typically indicates what a function returns rather than what errors might be thrown. This observation is indeed a fair point, as highlighted by Mark in the chat, who expressed that this realization makes him feel better about the lack of documentation surrounding errors in various libraries.
In reality, passing invalid inputs to most JavaScript libraries often leads to exceptions that are only tangentially related to the actual error made. For example, passing a primitive value where an object is expected in XState results in an uncaught type error, stating that one cannot use the in operator to search for context. This situation is quite humorous, as it indicates that if you pass a number or a string, the library does not even perform checks, leading to obscure errors.
JavaScript programmers are generally expected to recognize their mistakes earlier in the call stack and rectify their own issues, rather than looking for specific errors as one might in C. Unfortunately, this scenario does not seem likely to change anytime soon, unless one employs libraries like Effect or similar alternatives. In the context of using Effect, one must utilize it comprehensively for it to function effectively.
For instance, in our project, we are using the Effect web server to host the upload backend by creating middleware. When we utilize effect.gen, we are compelled to yield errors instead of merely throwing them. Consequently, when we consume these errors, we encounter code that handles different errors. Although there are too many use effects in this instance, we are leveraging Micro, a smaller version of Effect created by the Effect team.
In our implementation, we return micro TR promise, where we manage both the try and catch blocks, and we handle the errors accordingly. We are calling fetch, and if an error occurs, we remain uncertain about its nature due to the browser standards.
Mastering error handling in JavaScript means creating your own solutions to gain clarity and control over issues that arise, transforming chaos into manageable code.
In our project, we are focusing on the back end by creating a middleware. When we use effect.gen, we're forcing ourselves to not just throw errors but to yield them instead. Now, when we consume these errors, I find some code where we're handling different errors. There are too many use effects in this section; we are using micro for a bunch of functionalities. They made a special small version of effect for us, which I love.
Please ignore the type errors because I haven't installed anything here recently, so this is all just like out of types. We are calling micro, which again is just a smaller version of effect provided by the effect team. We return micro TR promise, where we handle the try and catch blocks. We turn this error into a custom format. Again, we are calling fetch, so if that errors, we don't know what it is. This could be a standard browser error, so we manually change the catch to take that error and create a new fetch error, instantiating it in the way we wanted it to be shaped.
Now, we pipe it to micro do map, which is the place where we actually assign the URLs we got from there and trace it with our fetch Tracer. When we call it, we know and can filter on good or bad states. So, we can call micro do filter or fail. If it's not okay, then we return the bad request error. When we map it, we won't have the errors; this is a way to filter out the ones that failed while still hoisting those errors and passing the success values down.
However, this required us to write our own custom fetcher, our own custom fetch error, and all of these things to glue this together. The code we are writing barely even looks like TypeScript anymore, but this comes with a massive win: we now know where and how errors occur and can manage them the way we choose. At every point throughout, we can handle those errors.
Has this code been hit? This code absolutely gets hit all the time. The result of this code being written is that we have a much better idea of when a user has an error with uploading things and where it came from because we own those error processes now. It's a really good thing for us to move to. It took a while, and I still barely understand it. I haven't had time to contribute to it much yet, but the team, specifically Mark and Julius, seem to really get it now, and the code they've been writing is awesome.
Unless you're using something like that, you're probably, as Ryan says, in a situation where there isn't a culture of strongly typed exceptions in JavaScript. Trying to apply that culture afterward is unlikely to produce satisfactory results. Why is it absent in the first place? Now we get to blame the language. The fun language capabilities lead to this culture being a predictable consequence of the way that JavaScript exceptions work. Without a first-class filtering mechanism to provide the ability to only catch certain exceptions, it doesn't make much sense to invest in formalizing error types that can't be usefully leveraged by developers.
Another reason these aren't really used much is that these sources of exceptions are not needed in the same way they are in other languages. Other languages have critical constraints that aren't present in JavaScript, such as pervasive explicit and imperative resource management. In those languages, every function needs critical cleanup code to ensure the correct long-running operations of the program, things like free and delete.
That's a fair point—if something fails in JavaScript, you can expect the garbage collection to deal with it going forward. In other languages, if something fails in a certain way, you need to deal with that and clean up the result to avoid memory leaks. Additionally, other languages don't really allow you to return different types of values from a function, so they have to handle errors in different cases instead. There's often lackluster support for first-class functions in those languages, whereas in JavaScript, you don't have to worry about any of that.
Apparently, he breaks down all of these points in detail. I feel like I understood those points already. If you want to better understand these concepts, I'll put this in the links so that this source will be in the description if you want to read this whole thing in detail.
Here, we have an interesting call-out of avoidable as well as unavoidable exceptions. Avoidable errors are logical errors in the calling code, like calling 32 on an empty array. These should never occur in production code and can always be avoided by calling functions correctly—it's a "you're holding it wrong" type of situation. However, there are unavoidable errors, like network issues.
Understanding the difference between avoidable and unavoidable errors is key to writing robust JavaScript code; avoid the former by calling functions correctly, and always prepare for the latter because they can happen anytime.
In the discussion about first-class functions in JavaScript, there is often a lackluster support for them. You have to worry about various issues, but apparently, he breaks down all of these in detail. I feel like I understood those points already. If you want to better understand these concepts, I will right now, before I forget, put this in the links so that this source will be in the description if you want to read this whole thing in detail.
Here we have an interesting call-out of avoidable as well as unavoidable exceptions. Avoidable errors are logical errors in the calling code, such as calling 32 on an empty array. These should never occur in production code, and you can always avoid them by calling functions correctly; it's a "you're holding it wrong" type of thing. However, there are unavoidable errors, like network sockets being closed during transmissions. You can't control these things, and they should always be considered possible. Programmers should always be aware that they might happen; in other words, something went wrong.
Now, let's talk about typed exceptions. Even setting aside the lack of exception typing in the wild, type exceptions are difficult to describe in a way that provides value in the type system. A typical use case for type exceptions would look like this: we try some code and we catch e. It would be nice if we knew the types of errors e could be. There’s a super type of what we think some code can throw, or we could automatically type it based on what errors we think some code could throw.
To understand the current state of support, we need to look at how TypeScript handles these cases today. Consider some basic exception inspecting code: if e is an instance of TypeError, we log it; if e is a string, we write it to uppercase; else, we throw it. Who else here? Let’s see some chatters. I know I'm not the only one who's written basically that exact code before. I want some ones in chat if you've written the like three checks on error: check if it has a message, if it doesn't, check if it's a string, and if not, you just say it because I know I'm not the only one who has done exactly that.
Look at all those ones! A lot of us have done exactly this. This code already works: e.message is strongly typed, and property access in e is correctly found. Even if e is any, e.toUpperCase() is strongly typed as well. More cases can be added, like detecting if e is an instance of RangeError. We accept as a broadly true principle that most JS code doesn't have documented exception behaviors. Most JS code has at least some indirection, so it can call code with undocumented exceptions. That’s an important point. Even if a given function has handled all of the errors it directly calls, it can still call something that can throw as well, and you have no guarantees there.
Thus, the whole hierarchy, top to bottom, has to have these types; otherwise, it's useless. Of course, safe handling of exceptions requires taking both of these into account. I agree with all of these principles. It's funny; in my head, when I was talking about this with Mark last night, I was thinking, "Why don't we have typed errors in TypeScript?" I was thinking through these things but wasn't specific enough to say, "Yeah, that's the case." In the end, I concluded that they probably should at this point; it can't be that much different than how chaotic a return is. I'm wrong; this is chaos.
As Ryan says here, if all of these things are true, which we can agree they are, then the right code to write and put into your codebase is the code that we had above. Since we can't actually know which of these types there are, even if we strictly type things more often, a throw, as CJ just said, like returns, doesn't bubble; throws do, though. So that thrown value from the wrong function could come all the way up, presenting real problems that we need to be considerate of. The only way to properly consider those in your codebase is to do this, so we're kind of stuck with this code.
These are very fair points. The default throw state of unannotated functions is that 100% of function declarations today don't have throws clauses. That’s also a fair point. If we added this throws clause, then we would have to make a decision about what to do with functions that don't have one. If it doesn't have one, it might be that it doesn't throw an exception, or it might be that it throws any exception. If we assume that these unannotated functions don't throw, the feature doesn't work until every type definition has an accurate throw clause. This is also fair because if one function throws, the other might not specify it.
Now, when we do this try-catch, this error...
To truly understand a complex issue, be open to changing your mind—define what evidence would shift your perspective.
A way to properly consider those in your code base is to do this: we're kind of stuck with this code. Very fair points arise when discussing the default throw state of unannotated functions. Currently, 100% of function declarations today don't have throws clauses. This is also a fair point; if we were to add this throws clause, we would need to make a decision about what to do with functions that don't have one. If a function doesn't have a throws clause, it might be that it doesn't throw an exception, or it might be that it throws any exception.
If we assume that these unannotated functions don't throw, the feature doesn't work until every type definition has an accurate throw clause. This is also a fair observation because, for example, if a function called "doSomething" throws an exception, we need to specify it. Now, when we perform a try-catch, this error is incorrectly typed. If we assume all unannotated functions do throw, the feature largely doesn't work until every type definition in the program has accurate throw clauses.
For instance, "doSomething" might throw an exception, while another function doesn't throw, but we didn't annotate it. As a result, the error has to be treated as unknown, even though we know this function doesn't throw. Therefore, we either have to annotate this as "throws never" or something similar, or we just blindly assume. Very fair points arise regarding assignability, which would also have to take into account throw information.
Consider the function "justThrow," which throws a new type error. If we pass this as a callback and call the callback, we need to ensure that the type we are passing specifies what errors could be thrown. This raises a significant concern. Getters and setters can also throw; I hadn't even thought of that. There are many very good points here.
Another related feature request is the ability to have checked exceptions, similar to Java or Swift. This feature requires that functions either catch specific exceptions or declare that they rethrow them. If a function calls another function that can error, it either has to handle that error or throw it up or a different error. You have to do something based on the errors. However, beyond Java and Swift, no other mainstream programming language has adopted these features.
The common opinion among language designers, including ourselves, is that this is largely an antifeature. In most cases, if I recall correctly, the Swift developer regrets this because of how much harder it makes the compiler and type checking work. Checked exceptions aren't seen in any of the widely used and liked languages today, with most new languages opting for something closer to the results type, which is a pattern in Rust. This is also similar to what they have in Go, or it follows a similar unchecked exception model.
Porting this feature to the JS ecosystem brings along a huge host of questions, particularly regarding which errors would be subject to checking and which ones wouldn't. The ES specification itself defines over 400 places where an exception is thrown, and it clearly doesn't make a hard distinction between avoidable and unavoidable exceptions because it wasn't written with this concept in mind. The distinction is also fuzzy in some cases; for example, a type error is thrown by JSON.stringify when encountering a circular data structure. In some sense, this is avoidable because many calls to JSON.stringify, by construction, cannot produce circularities, but other calls can.
It's not really clear if you should have to try-catch a type error on every call here. Fair point: if you know that what you're passing to JSON.stringify is fine, you shouldn't have to catch it. However, now you have to have the syntax able to specify that. The syntax error being thrown by JSON.parse if the input is valid might be impossible to handle or might not be. How do you specify those things? The same applies to regex; you have to force a try-catch around every single regex. The only way you could make regex worse is if you add try-catch to all of them.
So, what would change there? I love this; this is actually a really important thing. I think everyone should do this when they have a hard stance on something: you should be able to clearly state what it would take to convince yourself otherwise. For example, I think Flutter is a garbage pit dumpster fire, a terrible framework that no one should ever use for anything, but I can be convinced otherwise. I need to be shown a...
Be open to changing your mind; every strong opinion should have a clear path to reconsideration.
In programming, you have to have the syntax able to specify certain conditions, and the syntax error being thrown by json.parse if the input is valid might be impossible to have happen. Alternatively, it might not be clear how to specify those things. The same applies to regular expressions (Rex); you have to force a try-catch around every single Rex. The only way you could make Rex worse is if you add try-catch to all of them. So, what would change there?
I love this topic; it is actually a really important thing. I think everyone should do this: when they have a hard stance on something, they should be able to clearly state what it would take to convince themselves otherwise. For example, I think Flutter is a garbage pit dumpster fire, a terrible framework that no one should ever use for anything. However, I can be convinced otherwise. I need to be shown a Flutter app that I would actually use every day. That's all it takes. If somebody can create a good enough Flutter app that I actually use and don’t mind using, I will immediately go unlist all my videos about Flutter. I’ll even make a new one apologizing. But until I have that one good Flutter app on my iPhone, I’m going to continue saying it’s a garbage fire dumpster pit that nobody should touch.
Hopefully, you can understand what I’m saying here: every hot take you have should be accompanied by a clear statement of what it would take to change your mind. Until you show me that good Flutter app, I’m not changing my mind. Now, let’s see what it would take for the TypeScript team to reconsider type errors. Here are the two points they would consider that would make them change their mind: widespread adoption and documentation of strong exception hierarchies in JavaScript libraries in the wild. If everybody moved to effectively never throw, they would reconsider. Alternatively, TC39 proposals to implement some sort of pattern matching criteria to catch exceptions could also lead to a change. Arguably, this would just work by itself, the same way instanceof works today. This is, funny enough, the syntax that started this whole video.
In summary, any feature here implies a huge amount of new information in DTS files that isn’t documented in the first place. Good exceptions introspection, like catch (e) if e instanceof X, already works today, and anything more inferential than that is unlikely to be sound in practice. That’s why we don’t have it in TypeScript now; we just need to find the right syntax to have it at all. Apparently, we have some new syntax proposals, so let’s take a look at those.
Personally, I like try in the front or the back; I’m fine with either of those. I actually do like that option, but at that point, if you’re putting a keyword in front, you might as well just put it around a function. I don’t like the idea of another way to define variables; that’s my hesitation around try. We already have var, const, and let. I’m leaning towards try here, but the use of a keyword resulting in a tuple still feels a bit strange. Of these two, try is my personal choice for sure.
I hate the question mark at the end; it breaks the way you read through it. The other issue I have with both options is that they imply the function returns these two things. To be honest, my brain skips over the variable definition; I just blank on the thing on the left unless it says let, and then my brain clicks, making me pay more attention. Usually, my brain skips whatever’s on the left, so putting it there makes me auto-complete it as error data = might fail, which means I’m assuming might fail is returning this tuple, which it isn’t. When I go back to the source code and see it’s not, I get more confused. Putting a try here helps a bit with that, but at that point, you might as well just be calling a function like the safe A8 that I wrote.
This syntax change is tough. I want to do a poll in the chat: which syntax do you prefer, 1, 2, 3, or 4? It seems like people are leaning towards 2. My mistake was not doing the poll first; I prefer option 2 of all of these. God damn it! Gabriel, not all of us have an upside-down version of our keys on our keyboard! We’re going to start adding more upside-down keys, like what about an upside-down slash or upside-down period?
It seems like, within my audience at least, most of you agree with option 2 here. I get it; I don’t love any of these, but option 2 would be the better choice. Wouldn’t we have a lot of if error or error checks? Yes, and that’s great because now you’re being explicit about it instead of the code quietly erroring without you seeing it. That’s the benefit of Go; it’s tedious to have the if error is not equal nil checks constantly, but the benefit is that your code explicitly states what it’s doing. You have to do something with every error, and I like that.
Without my biases, people were still leaning towards try. Grab the snippet if you’re interested; I’m sure there are plenty of others. I’m pretty sure I had a cursor help me generate this one, if I recall correctly. I was getting hung up on trying to avoid a return type, but without one, no amount of as const was inferring properly down the line. So, the return type there is essential. It makes a pretty simple function that gives you these behaviors. If you like it, awesome! I liked it too, and if you don’t, that’s fine as well. Let me know what you guys think, and if you’re just going to wait for some syntax to get added, that’s cool too. I’m very curious how you all feel about this, so as always, leave it in the comments. Until next time, peace nerds!