In this thread, this is what I mean when I use these terms:
error = programmer error - you can catch them for logging purposes, etc, but you can never recover from them. I've been using the term "fatal error" interchangeably with "error" as the error should eventually be fatal. (e.g. InvalidParameter, AssertionError, etc)
exception = Something went wrong while doing a task, but the API user may choose to recover from the exception. (e.g. FileNotFoundException, IndexOutOfRangeException, etc)
I've also defined other phrases in the original post that people have been using back and forth, such as "Propagating an exception" and "Escalating an exception".
I previously talked about how simply "returning exceptions" isn't a great solution, because it doesn't self-document what exceptions are being returned when functions are simply propagating the exceptions down the call stack. I have recently realized that it doesn't have to be this way if you're using Typescript (Typescript isn't something I've had the opportunity to use much, but I would love to use it more - so excuse me if my Typescript example below isn't well-written).
With Typescript, one can declare what types of exceptions a specific function can return in each function signature, and Typescript can help make sure you handle all possible exceptions explicitly (usually). This sort of system certainly isn't perfect (and is probably a little verbose to use), but it is an improvement over what we've got today and might be enough for some people. Here's an example of how this might work:
// Exception utility class
class Exception <T> {
constructor(public code: T) {}
}
// Example API
interface User {
username: string,
}
// Notice how in the signature we explicitly declare each type of exception that can be returned.
// It gives this a similar feel to Java's checked exceptions, but without some of Java's issues.
function getUser(userId: number): User | Exception<'NOT_INITIALIZED' | 'NOT_FOUND'> {
if (!initialized) return new Exception('NOT_INITIALIZED')
const user = users.get(userId)
if (!user) return new Exception('NOT_FOUND')
return user
}
You can recover from all those kinds of errors, depending on the situation. "invalid parameter" could mean you handle it by showing the user an error in the UI.
This is true. Maybe I ought to stop giving example error/exception-types when defining them, as it's always situation-dependent. I think there's a bit of a fuzzy line here as to what is counted as an "exception" and what is counted as an "error", and ultimately it's up to the API designer to decide what constitutes as "misusing the API". The general rule of thumb I follow is "when in doubt, use an error. You can always turn them into exception later if a need arises to handle it (unless you're in Java)".
It's completely reasonable to design arrays that throw an "IndexOutOfRange" exception, allowing users to recover from this issue. It's also reasonable to design an array where the designers expect you to always check your index before indexing into it, and any out-of-bound issues will be thrown as an error. Yes it's possible for the user to recover from it, but that's not how the designers want you to deal with this issue.
There are some cases where an error will always be an error, like, if your program got itself into a bad state and you caught that issue (a common use for asserts).
So I misspoke a bit when I tried to define these two terms, thanks @ljharb for pointing this out. Maybe this is a better definition:
error = programmer error - you should never recover from these because they indicates there was a bug in the program (e.g. mis-used an API, program in a bad state, etc).
exception = Something went wrong while doing a task. If wanted, you can choose to recover from this issue.
I don't think it's possible for a function (eg) to be able to know, with authority, whether the caller can recover from it or not.
In other words, every single error, without exception (pun unavoidable) is potentially recoverable, and I suspect it simply wouldn't make any sense to attempt to classify some as "unrecoverable" from the implementation site.
No, it's not possible for the API to know if the caller can recover or not. It's up to the caller to escalate the exception as an error if it can't handle it.
Even if you can recover from an error, doesn't mean you should. An exception is a binding contract between the API and the user. This contract states that the function will always provide the same exception (no matter how it's refactored), and the exception will always leave the API in a good state. Any error handling without such a contract will surely be unstable, which is why it's deemed "unrecoverable" (or in my updated definitions, I phrased it as "should not recover") - not because it's necessarily impossible to recover from it, but because you shouldn't - instead, you should follow whatever other means the API provides to ensure that error does not happen, or maybe ask the API owner to make this binding contract and turn the error into a proper exception. (Some people attempt to recover from these anyways, by using a fragile regex to match against the content of the error message - a message that was intended to provide debugging details, and could change at a moment's notice).
This particular definition does mean that, for example, when you run eval('x=') and it throws a syntax error, this is really an exception, not an error. A contract is in place (by standards) that ensures this behavior stays consistent and things are left in a good state. There may be times when a user finds it useful to catch and handle this SyntaxError. The standard committee probably categorized it as an error because most of the time this gets thrown because there really was a bug in the code. If we were being more rigorous, we would just call it a SyntaxException and let the user escalate it as an error if they think it indicates a bug in their code - this is why a simple escalation syntax can be useful, and why in the original proposal I gave from the first post, escalation was the default behavior (it was implicitly done if it wasn't handled any other way).
An exception is a binding contract between the API and the user. This contract states that the function will always provide the same exception (no matter how it's refactored), and the exception will always leave the API in a good state
This is decidedly not how I've designed any API I've ever built, nor how any of the standard JS methods are built. It is widely understood that future versions might stop throwing an exception, for example. Where in the JS ecosystem is there a norm that a function will always provide the same exception?
In general, why would anyone ever provide an exception code if they don't expect you to programmatically catch and handle that specific exception? And if they're expecting you to do that, then why would they change that exception on you and break your code? How could I reliably write race-condition resistant code when reading a file, if I'm not supposed to catch and handle node's file-not-found exception (because at any point they may choose to just stop throwing that exception, causing my code to break).
Maybe it was wrong for me to call a SyntaxError an exception if I'm supposed to understand that at any point the standard committee may change the type of error being thrown at a moment's notice. However, there's got to be some set of stable exceptions that are treated as part of a function's API that I can expect to always be present, otherwise, there's really no such thing as being able to reliably catch an exception, handle it, and continue execution.
As far as the language is concerned, and most of user land, that’s not the case. node is a bit different - but its error codes don’t mean that error will always occur, it just means that if it does, you can handle it. It also doesn’t preclude new kinds of errors from being added later.
Why is node a special snowflake here? What's is it doing differently that makes it important for node to have well-documented exceptions and no one else?
Doesn't it? If I open a file, and the file doesn't exist, isn't it Node's responsibility to throw the file-not-found exception? If it only sometimes throws it, and it's not well documented the time it will and won't, then how can I ever rely on this behavior? If Node's exception behavior is inconsistent, then the only way I can reliably handle opening a potentially non-existing file is checking for its existence in advance, which exposes my code to race conditions.
You are certainly right here. This is one thing I dislike about Java's checked exceptions is that any new exception you add to the API becomes a breaking API change (even if its addition doesn't actually break anyone's code). Any exception system we design should keep this principle in mind.
I think this would be very useful in the domains of TypeScript and Node.js.
I've got a game with a TS/Node backend. When using some database calls in the server, if the database call fails for some reason (bad data being set, user does not exist, database transaction failure) it throws an error. If I don't catch it, the error bubbles up to Node.js and a 500 INTERNAL error is thrown. This is not great for the player, as it doesn't provide any good information.
I'd like to be able to have exceptions so that I can handle it in my code ahead of presenting it to the player. If I could get a set of possible exceptions (BadDataException, UserDoesNotExistException, TransactionException), I could handle each independently. As of right now, I could try to parse the error message, but the example below suggests that's not a great solution. I do support the effort to add named exceptions to the language.
try {
await db.update(dbReference, myData)
} catch (e) {
if (e.message.includes('not found')) {
// User does not exist
...
} else if ...
}
I would actually like if I, as a developer, had to handle these exceptions. TypeScript could enforce that option as part of the build, which would make my code more robust and reliable. But I do understand that it's not something everyone would want.
I wonder if it's something that we could stick in JSDoc, which would provide semantic information to the parser/linter while also allowing the developer to get the additional exception context in their IDE.
/**
* Stick data in database
*
* @param userId - The player identifier
* @param data - The data to be updated
* @throws BadDataException - If the data is null
* ...
*/
async function stickInDatabase(userId, data) {
...
}
For those writing in generic JavaScript, they may prefer to escalate all exceptions as errors and that should be able to work without the API consumer changing any syntax. It should also let advanced developers to provide this information to API consumers. At that point, it should not entirely be a major breaking change but a minor API release as per semantic versioning if there needs to be generic catch handler as part of the grammar.
try {
await stickInDatabase(userId, data)
} catch 'BadDataException' (ex) {
throw new Error('Your data is invalid. Please change the data you are sending.')
} catch (ex) {
// This error catches literally anything else, both unchecked exceptions and existing errors
throw new Error('Something strange happened')
}
If the library throws nothing but Error with a message, that's a deficiency of the library, not the language. It could throw different types for different kinds of errors. JS lacks syntax to catch by type, but you can branch on instanceof today.
So if I understand you right, it sounds like the main feature you're wanting is an opt-in ability (through a linter or typescript) that requires the coder to explicitly handle each exception a function might throw. Such a feature request rubs into the following issue: An API developer may wish to turn what used to be an error into another exception. This kind of change should not be a breaking change, as technically no behavior has changed, so API consumers shouldn't be required to update their code. This is one of the criticisms against checked exceptions that we need to figure out how to handle differently. I don't know the best way to deal with this, so it can be a good conversation to have (my original proposal dealt with it by not having this feature, but this seems to be the primary feature you are hoping for, which is understandable - I can see the value in it).
Also, just to make sure I understand you right, your ideas of using the JSDoc comments is mostly a "trust me" system, where you document what might be thrown, maybe some minimal linting can help prevent basic mistakes, but for the most part, it'll have to trust you? And I presume these jshint comments would only be applied on the public-facing functions?
@ljharb - I decided to look around for other examples of exceptions from the Javascript community outside of node. Here are a handful of examples I found.
Here's one from Google's firebase - When attempting to sign in, you might fail because multi-factor is needed. This causes an exception to be thrown with the code 'auth/multi-factor-auth-required', which you can then catch and handle. If this exception was unreliable, then any logic bringing a user to a second-factor screen would also be unreliable.
The axios library is another example. When a request fails, it's documented that an exception will be thrown with specific properties, like the response code or response body. If you expect that the request might fail (e.g. with an unauthorized code), then you need a reliable way to get that information from axios so that you can, for example, redirect the user to a log-in screen. If this exception was unreliable, then you wouldn't be able to reliably redirect a user to the login screen when appropriate.
When you use an abortController to abort a fetch request, that request is supposed to reject with an AbortError. If this exception is unreliable, then you wouldn't be able to reliably ignore abort exceptions when reporting other errors to the end-user. (update: This one might have been a bad example, it has "error" in the name, so maybe I'm not supposed to rely on this error always staying the same).
I'm sure when these API designers documented these exceptions, it's because these exceptions are part of the API, and it'll receive the same stability as any other part of the API. They want us to feel at ease with catching and handling them, knowing the exception code/type won't suddenly change, and it'll always throw the appropriate exception, 100% of the time (if this weren't the case, how would firebase expect us to reliably build two-factor auth?).
I understand the distinction you're trying to make between "Error" and "Exception", however I think it's a really unfortunate choice of words. The term "error" is extremely broad, whereas "exception" fairly specific, yet they are often used interchangeably because various languages / libraries mingled them up ages ago. Java may have the weird luxury of having all exception class names suffixed with "Exception", but other languages don't.
In Python, it is customary to use suffix "Exception" for base classes (BaseException, Exception, HTTPException, ...) and then suffix "Error" (LookupError, AssertionError, ...) or no suffix (KeyboardInterrupt, QueueFull, BadRequest, ...) for concrete exception subclasses. In C++ we have exception as the customary base class, and then subclasses with or without _error suffix for concrete exceptions.
Funnily enough, the language where this is most confusing happens to be JavaScript: throw new Error() means "throw an exception of type Error".
throw new Cake() means "throw an exception of type Cake". What is being thrown is-a Cake and is-an Object, and that's it. We refer to it as an exception not because of what it is, but because of the mechanism by which it travels through the call stack. Once you catch the Cake, it becomes a regular value, which you can manipulate, print, return, re-throw to make it an exception again, or wrap in another type and throw that instead.
Now with terminology cleared up, I still don't understand why one particular subclass of Error should have special language-level properties. Imagine we have two ordinary subclasses of Error, serving as base classes for different kinds of errors purely by convention:
LogicError — errors due to buggy code, that could be prevented by fixing the code
RuntimeError — errors due to runtime circumstances, that cannot be prevented in code
Rewriting your example without new language features:
function getUser(id) {
if (!user.has(id)) {
throw new NotFound('User was not found') // extends RuntimeError
}
if (!serviceAvailable()) {
throw new ServiceUnavailable() // extends RuntimeError
}
return users.get(id)
}
function getUserOrDefault(id, defaultValue = null) {
try {
// The new `excepts` keyword is redundant because the exceptions
// you anticipate are all named in the catch block, and all unanticipated
// exceptions fall through to the end of the block where you escalate
return getUser(id)
} catch (ex) {
// We can catch and handle the NotFound exception like you would expect
if (ex instanceof NotFound) {
return defaultValue
}
if (ex instanceof ServiceUnavailable) {
return defaultValue
}
// If getUser() happened to throw an exception that wasn't 'NotFound' or
// 'ServiceUnavailable', it would be escalated, and an instance of Error
// would have been thrown instead, explaining what happened.
throw unhandled(ex);
}
}
function unhandled(ex) {
if (ex instanceof RuntimeError) {
return new UnhandledRuntimeError(ex); // extends LogicError
}
return ex;
}
Now you only need a linter checking that all your catch blocks do something on every possible code path. Making this behaviour implicit, without having to throw unhandled explicitly, is the only benefit of a specially treated Error subclass that I can see.
Yes - I only intended this distinction between error and exception to exist in the context of this thread, as we need some sort of verbage to distinguish the two ideas, and "error" and "exception" seems to be the most common way of doing so, even if in many other contexts people treat these words as the same. I'm mostly taking my definitions from the dart language (see here) where error just means "programmer error" and exception means "anything else" - you seem to be saying that exception should mean something different, where anything that gets thrown is an exception. If it makes things clearer, we could start using "logic error" and "runtime error" to make the distinction - we just need something to make the distinction.
Here's one reason why it can be useful to treat "exceptions" or "runtime errors" special at the language level. What you're proposing will be extremely verbose if all you're wanting to do is propagate exceptions from the function you're calling to your caller. Here's an example from the proposal-idea-comparison github repo.
export function setProperty(userId, propName, value, { userIdDoingRequest }) {
assertUserHasPermission(userIdDoingRequest, PERMISSIONS.update) excepts 'Unauthorized'
const user = getUser(userId, { userIdDoingRequest }) excepts 'Unauthorized', 'NotFound'
if (!Object.prototype.hasOwnProperty.call(user, propName)) {
throw new Exception('MissingProp')
}
user[propName] = value
}
Compare this with the solution you're suggesting
export function setProperty(userId, propName, value, { userIdDoingRequest }) {
try {
assertUserHasPermission(userIdDoingRequest, PERMISSIONS.update)
} catch (ex) {
if (ex instanceof UnauthorizedError) {
throw ex
}
throw unhandled(ex)
}
let user
try {
user = getUser(userId, { userIdDoingRequest })
} catch (ex) {
if (ex instanceof UnauthorizedError || ex instanceof NotFoundError) {
throw ex
}
throw unhandled(ex)
}
if (!Object.prototype.hasOwnProperty.call(user, propName)) {
throw new MissingPropError()
}
user[propName] = value
}
This isn't an issue if all you're wanting to do is have a better exception system for handling exceptions thrown at the domain boundary - i.e. you're calling an external library, it's known that certain functions throw certain exceptions, and you want to be required to handle them all when you use this library. This sounds similar to what @Fleker was proposing. I can see the value in that. What I've been hoping to focus on a little more in this thread was how to improve exceptions for the API creator (not the consumer). i.e. the exception might be created from within your own application and handled higher up, or it might be handed off to the end-user as part of your public API. This, to me, is the most fragile part of dealing with exceptions. (I still welcome ideas to improve the end-user experience though)
The other issue is that a linter isn't going to be 100% effective at this, as it's impossible to always figure out where a function comes from. (This doesn't mean I'm opposed to having linter suggestions, but it's important to realize that a linter has significant limitations).
But in general, if exceptions/runtime errors are so commonplace today, you would hope that we can provide some better language-level support for them than the fragile system we've got today.
Yes I would like to have some way, perhaps opt-in, to know that I'm handling exceptions. I do think it can be done in a backwards compatible way if there's some way to catch unhandled errors/exceptions so that the builds don't fail entirely.
I do like the clean syntax approach in a similar vein as async/await. At the same time, you could easily start to except a lot of exceptions depending on the level of your stack. Is there some other way to suggest that a call may throw an exception without naming each one? Does it help for all of them to be named, or is this something that can be done better in external tooling?
If all I want to do is propagate exceptions to the caller, then I'm not wrapping the call in a try block. What's the point of this excepts clause outside a try block?
const user = getUser(userId, { userIdDoingRequest }) excepts 'Unauthorized', 'NotFound'
Why would I copy & paste a list of exceptions from getUser documentation without handling any of them? This line of code apparently doesn't care what getUser throws, it bails out if anything goes wrong. If one day getUser starts throwing GoneFishingTryAgainNextWeek, you'll have to go and patch every call site, just to silence annoying "hey! you're not handling this new exception on this line that's also not handling any of the old exceptions". In my opinion, the excepts clause makes this code more fragile.
Do you buy the argument that exceptions should be part of the function's API? i.e. they're stable - if the function stops throwing an exception or throws a different exception instead, then it's a breaking change. Otherwise, how can anyone rely on them? The only thing you could do is treat them as an error and simply report the failure without trying to recover.
With that in mind, here's why Javascript's current system of invisibly auto-propagating exceptions is bad and fragile. (It works great for errors, which is what it was designed for, but it's horrible for exceptions)
It's near impossible to figure out what exceptions a particular function might throw. How can you keep the API stable if you don't even know what the API is?
It's difficult to refactor code without making breaking changes. You want to make a public-facing function stop using function f internally, and use g instead, as it better suits what you're trying to achieve. Because it's difficult to know what exceptions f() and g() throw, it will be difficult to know if this change will be a breaking change or not.
They're difficult to contain and control. Throwing a new exception deep down in the low levels of your project can cause who knows how many of your public-facing functions to start throwing that new exception. The public will stumble into these new exceptions and start relying on them because it's reasonable to expect that an exception with a distinguishing code is supposed to be able to be singled out, caught, and handled. You've accidentally added exceptions to your API (not a breaking change) and can't remove them without it being considered a breaking change. As an example: Maybe you have an internal-use-only exception "BadResponseBody" that was never intended to be used by the public. You send the response body of a REST request to a parseResponse function, which might throw BadResponseBody, and you forgot to escalate it in this specific scenario (isn't invisible auto-propagation great? This is easy to forget). Now your public getStuffFromServer() function could start throwing a BadResponseBody exception whenever it connects to a misconfigured external server, and your end-users might start relying on this exception to detect a misconfigured server.
Hopefully, it's clear that if exceptions are part of our API, then these three points are big issues with invisible, auto-propagation of exceptions. There are many ways to fix these issues, some of which have been discussed in this thread as alternatives to my proposal. All of these solutions have different downsides, so it's a question of which one works the best in Javascript. Java solves this issue with checked exceptions (turning invisible, auto-propagation to mostly visible, auto-propagation), Rust and other functional languages have "either monads" (many people love this system, some find it too verbose outside the functional world), and then there's my proposal.
My proposed solution
The way my proposal solves this issue is by making auto-escalation the default behavior for exceptions, instead of auto-propagation. If you want to propagate an exception, you have to explicitly do so with the excepts keyword. (meaning even if you throw a NotFound exception in one function, it'll be received as a generic Error instance by its caller, unless its existence was explicitly recognized with the "excepts" keyword). Any exception you do not wish to handle or propagate should simply be omitted from the exceptions list.
Observe how neatly it solves the three issues I brought up about Javascript's invisible auto-propagation:
It's not that difficult to figure out which exceptions a particular function can throw. Any exception it can throw must be explicitly named somewhere in the function body.
It's very easy to refactor your higher-level code without making breaking changes. If you want to swap f() for g(), you know exactly which exceptions were being propagated from f() (it's in the "excepts" clause), and you can ensure that you propagate the same exceptions from g(). If g() throws any extra exceptions, you can be sure to leave them out of the excepts clause, so they auto-escalate and don't change your API (unless you want to start supporting these extra exceptions in your public API). If g() doesn't throw enough exceptions, then you may have to make some adjustments to make sure all exceptions are accounted for.
They're easy to contain and control. If you decide to change what used to be an error into a new GoneFishingTryAgainNextWeek() exception deep down in your codebase, it won't change a thing unless you've explicitly acknowledged the existence of the exception. Without acknowledging its existence, its default behavior is to escalate it into an error, and since it used to be an error anyways, no public APIs will change (unless you explicitly change them by acknowledging the existence of this exception).
Responses to your concerns
The reason is because we're changing the default behavior of exceptions to auto-escalate instead of auto-propagate, so it makes sense to use the excepts keyword outside a try block. This is the only way to propagate it, and it's designed this way because invisible auto-propagation causes a lot of problems.
It does care what getUser() throws. It specifically cares about the fact that getUser throws both an Unauthorized and NotFound exception, and it doesn't care about anything else. The purpose of this "excepts" clause wasn't to propagate all the exceptions that getUser() throws, it was to specifically propagate just those two, which happen to also be the only two that getUser throws. But if getUser decided one day to start throwing GoneFishingTryAgainNextWeek too, then lucky for us, it won't be in our excepts clause, so it will be auto-escalated. No API change will happen in this function without us explicitly changing this function. We won't accidentally start exposing a new exception as part of our public API (if this were a publicly exposed function, or used by one).
Some final notes
I don't want to claim that my proposal should be the solution to this problem. I do like it, and I do think it solves a number of problems very well, but I also understand it has some shortcomings, and know that people might find more comfort in a familiar checked-exception way of solving this issue (like @clemdz proposed). Something needs to fix the broken exception system, and I don't know what that fix will be yet.
I also want to clarify something thing that I never said, but should have. I don't know if you were looking into the proposal-comparison repo or not, but I intended all of the functions (in both userManagement.js and exampleUsage.js) to be seen as part of the same project. If functions such as getUser() were part of a third-party API, then I would write the exception-handling code differently from what's being shown in that repo (by translating the exceptions from one domain to the next).
As one final thing - thanks for this conversation - it's helped me better articulate what the current problems are in Javascript and what it is we're actually trying to solve. Those three points I listed above are the issues with Javascript's exception system that needs to be fixed, one way or another. Some other points I've talked about previously might better be categorized as nice-to-have ideas in a new exception system, but it isn't the specific problem we're solving. It's good for a thread to have a focus, and in this case, I think a good focus is "how to best solve those three issues in Javascript".
This is a fair point and a good concern. There may be ways to reduce the number of exceptions you have to specify. For example, if a lot of functions throw both "NotLoggedIn" and "InsufficientPermissions", then maybe a syntax shortcut can be made to allow you to predeclare a list of exception types. As a rough example
// This is not declaring a variable. This is just binding a list of exception names to a single
// name that we can then use in an excepts clause.
exceptions baseExceptions = 'NotLoggedIn', 'InsufficientPermissions'
function doThing() {
f() excepts ...baseExceptions
g() excepts ...baseExceptions, 'AnotherException'
h() exceptions 'NotFound'
}
It could even be possible for a module to export a set of common exceptions, and have other places import and use those exception sets. e.g. maybe there are 6 different types of IO exceptions, and many functions from a particular module can throw any of them, so it can be convenient if most of the time think the variations of IO exceptions were thought of as a single entity (and used in except clauses as "excepts ...ioExceptions"). I'm certainly not sold on this idea or syntax, but it at least shows that there are a few ways to make things a bit less verbose.
Another (or additional) solution would be to simply have an "excepts *" shorthand, that indicates that all exceptions that particular function throws should be propagated. I'm a bit hesitant about adding such a feature because it's easy to overuse and flies against the face of some of the problems we're trying to solve. But it'll probably be a necessary construct.
External tooling can not help here. The point isn't to name all of the exceptions in the excepts clause, rather, we're naming the ones we want to propagate or handle, any others should not be named and should be escalated. In other words, we're trying to be very deliberate about what exceptions each function will or will not throw, filtering down irrelevant exceptions with each excepts clause (by not including irrelevant ones in the list of exceptions, causing them to escalate)
I noticed in this link@aclaymore posted about trying to add type-safety for exceptions in typescript, that they were often going back and forth about whether or not a function's "throws" list should be auto-calculated by looking at all of the possible things the function can throw (by following everything it calls). I would be adamantly against such an idea as I feel it goes against what an exception is supposed to be. If I called JSON.parse('{}'), tooling could realize that JSON.parse() may throw a SyntaxError (even though it never would on the input '{}'), and my function would automatically inherit SyntaxError as one of the types of exceptions people are forced to handle, even though there's nothing to handle, and SyntaxError was never supposed to be an intended exception. It's much better if we're explicit about the kinds of exceptions something should throw, so the compiler can know that no other kinds of exceptions should go through it. This same principle applies to both Typescript and Javascript.