Repository navigation
Suggestion: throws clause and typed catch clause #13219
Description
Activity
- changed the title
[-]Suggestion: Checked exceptions and typed cache clause[/-][+]Suggestion: Checked exceptions and typed catch clause[/+]on Dec 29, 2016 - addedSuggestionAn idea for TypeScriptAn idea for TypeScript
on Dec 30, 2016 DanielRosenwasser commented
on Dec 30, 2016 MemberMore actionsJust to clarify - one the ideas here is not to force users to catch the exception, but rather, to better infer the type of a catch clause variable?
Reacted by ExE Boss, Ivan Wang, Finbel, resynth1943, Chet Corcos, Benjamin Rood, luisgurmendezMLabs, Thomas Gauvin, Pulkit Sood, Nino Filiu and 74 more- addedIn DiscussionNot yet reached consensusNot yet reached consensus
on Dec 30, 2016 Daniel Rosenwasser (@DanielRosenwasser)
Yes, users won't be forced to catch exceptions, so this is fine with the compiler (at runtime the error is thrown of course):function fn() { throw "error"; } fn(); // and try { fn(); } finally { // do something here }But it will give developers a way to express which exceptions can be thrown (would be awesome to have that when using other libraries
.d.tsfiles) and then have the compiler type guard the exception types inside the catch clause.Reacted by Felix Becker, Joe Pea, Akash Gupta, Rasmus Göransson, Lukas Troyer, Senyaak, Bartosz Brzeziński, Paweł Szymański, Robert K. Bell, David Cook and 62 moreReacted by Joe Pea, Federico Bevione, Andre Byrne, David Cook, Finbel, Thomas Gauvin, Eugene, Uladzimir Kasacheuski, Gabriel Théron, Peeyush Kushwaha and 9 morehow is a checked throw different from
Tried<Result, Error>?type Tried<Result, Error> = Success<Result> | Failure<Error>; interface Success<Result> { kind: 'result', result: Result } interface Failure<Error> { kind: 'failure', error: Error } function isSuccess(tried: Tried<Result, Error>): tried is Success<Result> { return tried.kind === 'result'; } function mightFail(): Tried<number, string> { } const tried = mightFail(); if (isSuccess(tried)) { console.log(tried.success); } else { console.error(tried.error); }
instead of
try { const result: Result = mightFail(); console.log(success); } catch (error: Error) { console.error(error); }
Reacted by David Wickström, Kirill Agalakov, Paweł Szymański, Anton Petkov, Bartosz Brzeziński, Diego Maroto, Joe Percy, Alain Perkaz, Daniel Kaplan, Magnus and 4 moreReacted by Dominik Lach, Jesse Schalken, ebaersoft, cojack, Cipriano Sánchez, sjkillen, cocowalla, Leon Adler, Jose Luis Garcia Ott, Jarrod Mosen and 157 moreReacted by Ognjen AndricReacted by Joe Pea, Wilhelm Olejnik, Nishant Neeraj, Egor Andrianov, Jesse Jackson, Ravi van Rooijen, Christopher Francisco, Matthew Hill, MatthiasEngh, Eugene and 5 more- addedAwaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this featureand removedIn DiscussionNot yet reached consensusNot yet reached consensus
on Jan 10, 2017 You're suggesting not to use
throwat all in my code and instead wrap the results (in functions that might error).
This approach has a few drawbacks:- This wrapping creates more code
- It requires that all chain of invoked function return this wrapped value (or error) or alternatively the function that gets
Tried<>can not choose to ignore the error. - It is not a standard, 3rd party libraries and the native js throw errors
Adding
throwswill enable developers who choose to to handle errors from their code, 3rd libraries and native js.
As the suggestion also requests for error inferring, all generated definition files can include thethrowsclause.
It will be very convenient to know what errors a function might throw straight from the definition file instead of the current state where you need to go to the docs, for example to know which errorJSON.parsemight throw I need to go to the MDN page and read that:Throws a
SyntaxErrorexception if the string to parse is not valid JSONAnd this is the good case when the error is documented.
Reacted by Boris Cherny, Louis-Dominique Dubeau, cocowalla, Timothy Soehnlin, Badger, Andrey Agibalov, Janis Reber, Wessel Kronemeijer, sunil, Max Holmark and 142 moreReacted by codexpansion, luisgurmendezMLabs, Pedro Torchio, Damien Golding, Eugene, Uladzimir Kasacheuski, Corey Pyle, Jo, Aleksandar Markovic, Walter Zimmerman and 5 moreAnd this is the good case when the error is documented.
is there a reliable way in javascript to tell apart SyntaxError from Error?
-
yes, it's more code, but since a bad situation is represented in an object, it can be passed around to be processed, discarded, stored or transformed into a valid result just like any other value
-
you can ignore tried by returning tried too, tried can be viewed as a monad, look for monadic computations
function mightFail(): Tried<number, string> { } function mightFailToo(): Tried<number, string> { const tried = mightFail(); if (isSuccess(tried)) { return successFrom(tried.result * 2); } else { return tried; } } -
it's standard enough for your code, when it comes to 3rd party libs throwing an exception it generally means a gameover for you, because it is close to impossible to reliably recover from an exception, reason is that it can be thrown from anywhere inside the code terminating it at an arbitrary position and leaving its internal state incomplete or corrupt
-
there is no support for checked exceptions from JavaScript runtime, and i am afraid it cannot be implemented in typescript alone
other than that encoding an exception as a special result case is a very common practice in FP world
whereas splitting a possible outcome into 2 parts:
- one delivered by the return statement and
- another delivered by throw
looks a made up difficulty
in my opinion, throw is good for failing fast and loud when nothing you can do about it, explicitly coded results are good for anything that implies a bad yet expected situation which you can recover from
Reacted by Kamil Gajowy, Anton Petkov, Steve Officer, Alaa Moucharrafie, amccall-eigt, Iulian Pleșoianu, mooo, Dominik Bucher, Ryan Perry-Nguyen and Yuri KostinReacted by Josh Ghoulberg 👻, Senyaak, Kevin Montag, Gabriel Rinaldi, Simon Weaver, Bert Verhelst, Patrick Roberts, Steve, someniatko, Herriau and 41 more-
consider:
// throw/catch declare function doThis(): number throws string; declare function doThat(): number throws string; function doSomething(): number throws string { let oneResult: number | undefined = undefined; try { oneResult = doThis(); } catch (e) { throw e; } let anotherResult: number | undefined = undefined; try { anotherResult = doThat(); } catch (e) { throw e; } return oneResult + anotherResult; } // explicit results declare function doThis(): Tried<number, string>; declare function doThat(): Tried<number, string>; function withBothTried<T, E, R>(one: Tried<T, E>, another: Tried<T, E>, haveBoth: (one: T, another: T) => R): Tried<T, R> { return isSuccess(one) ? isSuccess(another) ? successFrom(haveBoth(one.result, another.result)) : another : one; } function add(one: number, another: number) { return one + another; } function doSomething(): Tried<number, string> { return withBothTried( doThis(), doThat(), add ); }
Reacted by Val AkhmedovReacted by Ali Naci Erdem, Ilya Yurchenko, Nicholas Main, Ervin Racz, Tomáš Hübelbauer and ÅlexReacted by Dmytro Parzhytskyi, reinik21, Lucas, Eric Ferreira and ÅlexMy point with
JSON.parsemight throwingSyntaxErroris that I need to look the function up in the docs just to know that it might throw, and it would be easier to see that in the.d.ts.
And yes, you can know that it'sSyntaxErrorwith usinginstanceof.You can represent the same bad situation with throwing an error.
You can create your own error class which extendsErrorand put all of the relevant data that you need in it.
You're getting the same with less code.Sometimes you have a long chain of function invocations and you might want to deal with some of the errors in different levels of the chain.
It will be pretty annoying to always use wrapped results (monads).
Not to mention that again, other libraries and native errors might be thrown anyway, so you might end up using both monads and try/catch.I disagree with you, in a lot of cases you can recover from thrown errors, and if the language lets you express it better than it will be easier to do so.
Like with a lot of things in typescript, the lack of support of the feature in javascript isn't an issue.
This:try { mightFail(); } catch (e: MyError | string) { if (e instanceof MyError) { ... } else if (typeof e === "string") { ... } else {} }Will work as expected in javascript, just without the type annotation.
Using
throwis enough to express what you're saying: if the operation succeeded return the value, otherwise throw an error.
The user of this function will then decide if he wants to deal with the possible errors or ignore them.
You can deal with only errors you thrown yourself and ignore the ones which are 3rd party for example.Reacted by ebaersoft, Leon Adler, Greg Rozmarynowycz, Badger, Janis Reber, MarvinHannott, Wessel Kronemeijer, Felix Becker, Juan Pablo de la Torre, Luis Pais and 28 moreReacted by Corey Pyle and Bernardo da Eira Duarteif we talking about browsers
instanceofis only good for stuff that originates from the same window/document, try it:var child = window.open('about:blank'); console.log(child.Error === window.Error);
so when you do:
try { child.doSomething(); } catch (e) { if (e instanceof SyntaxError) { } }
you won't catch it
another problem with exceptions that they might slip into your code from far beyond of where you expect them to happen
try { doSomething(); // <-- uses 3rd party library that by coincidence throws SyntaxError too, but you don' t know it } catch (e) {}
Reacted by Aluan Haddad, Ethan Resnick, Omri Luzon, Dmitry Mazurok, ExE Boss, Jesse Jackson, Qwerty (Vítězslav Ackermann Ferko), Scotty Jamison, Artur Baybulatov, Alaa Moucharrafie and 5 moreReacted by Wessel Kronemeijer, Gitowiec, Josh Ghoulberg 👻, Herriau, Daniel Bulant, Tommy Troy Lin, Alexander Satretdinov, Stewart McGown, BalaM314, reinik21 and 3 morebesides
instanceofis vulnerable to prototype inheritance, so you need to be extra cautions to always check against the final ancestorclass StandardError {} class CustomError extends StandardError { } function doSomething() { throw new CustomError(); } function oldCode() { try { doSomething(); } catch (e) { if (e instanceof StandardError) { // problem } } }
Reacted by Aluan Haddad and Kamil GajowyReacted by Wessel Kronemeijer, Gitowiec, Josh Ghoulberg 👻, Wilhelm Olejnik, Herriau, Daniel Bulant, Derek Pavao, Tommy Troy Lin, Stewart McGown, BalaM314 and 6 moreAleksey-Bykov Explicitly threading errors as you suggest in monadic structures is quite hard and daunting task. It takes a lot of effort, makes the code hard to understand and requires language support / type-driven emit to be on the edge of being bearable. This is a comment comming from somebody who puts a lot of effort into popularising Haskell and FP as a whole.
It is a working alternative, especially for enthusiasts (myself included), however I don't think it's a viable option for the larger audience.
Reacted by Jose Luis Garcia Ott, Jarrod Mosen, Aluan Haddad, Felix Becker, Anup Kishore, Wessel Kronemeijer, Ethan Resnick, interphx, armando magalhaes, Dmitry Sabanin and 33 moreReacted by codexpansion, Eugene, Uladzimir Kasacheuski, Aleksandar Markovic and Rägnar O'ock261 remaining items
Load more actionsRyan Cavanaugh (@RyanCavanaugh) would it be possible to reopen this please? Thank you
I don't think there's any actionable items in regards to this specific issue at this time. I think the next step should revolve around implementing a solution that encourages error checking in hopes to create widespread adoption of error documentation. After that point I imagine this issue could be brought back up, though I am definitely curious about their thoughts on our recent comments.
I'm going to try my hand at a linter rule over the weekend. If it goes well, I'll link the repo back here and we can move that conversation there.
I think it’s important enough to not be closed and encourage more discussion
Agreed Kyle Kashuba (@KashubaK), I think the TypeScript team has made their opinion known. While I personally don't agree with the decision to deprioritize exception handling, I understand it. They don't think people will adopt it, and so it's on the community to prove otherwise. (That being said, I'd love if they reconsidered :))
Their reconsideration points made that clear:
Reconsideration Points
What would change our evaluation here? Two primary things come to mind:- Widespread adoption and documentation of strong exception hierarchies in JS libraries in the wild
- TC39 proposals to implement some sort of pattern-matching criteria to catch exceptions (arguably this would just work "by itself" the same way instanceof works today)
Kyle Kashuba (@KashubaK), your steps sound right to me. And your idea of integrating with DefinitelyTyped would be a massive step towards adoption (and maybe the only step needed to satisfy reconsideration point 1).
If you go with the ESLint approach, it looks like eslint-plugin-jsdoc already has the first bullet point (via
requires-throw). And can be a good source of inspiration.As far as the spec of how exactly it should work
- It might be valuable to draft an RFC that covers how it will work (kind of like how it's outlined in the md) - if you're interested in input.
- It might be easier to forego having a "safe" throws in the first version (if you're looking to shave off scope). And have a stance on checked vs unchecked exceptions.
- Swift enforces checked exceptions (you have to specify that you throw and you always have to catch or declare throw)
- Kotlin has no checked exceptions (you can throw, but there's no enforcement you catch - like TypeScript)
- They support
@throwsannotation for interop
- They support
- Java has checked exceptions (with some special unchecked ones)
Java seems closest to the hybrid of supporting both (safe and unsafe). But I'd personally advocate for matching swift (checked exceptions), but with the ability to opt into
- None, warning, and error versions of enforcing you declare
@throws - None, warning, and error versions of enforcing you catch something that declares
@throws
Reacted by Zefir and Kyle KashubaReacted by LifeIsStrangeThanks for your input Kyle Venn (@kvenn)!
The reason I wanted to introduce "safe" exceptions is to address an important note mentioned earlier:
Constructing a RegExp from non-hardcoded input is so vanishingly rare that it seems obnoxious to force a try/catch around new RegExp("[A-Z]") since that technically "can" throw an unavoidable exception, even though by inspection it clearly doesn't.
But perhaps it would be more dangerous and confusing than helpful. Some configuration to whitelist certain functions might be appropriate instead.
Regarding an RFC, for something like this I wouldn't hesitate, but I have some ideas that I need to make sure are even feasible before I potentially waste others' time. Regardless I would love a community effort, as it will be required to move the needle in any meaningful direction.
Reacted by Kyle Venn and Justine Che T. RomeroI think it’s important enough to not be closed and encourage more discussion
Personally I agree. That being said, as Kyle Venn (@kvenn) stated, the TS team has already made their stance clear. Robust error handling needs to be normalized in the community before such an invasive new concept is added to countless others' workflows. If our ideas aren't able to proliferate, well, perhaps they were doomed to begin with.
Reacted by Kyle VennConstructing a RegExp from non-hardcoded input is so vanishingly rare that it seems obnoxious to force a try/catch
It would be simpler to just use
// @ts-expect-errorthan introducing a whole new error handling concept.Reacted by Kyle VennConstructing a RegExp from non-hardcoded input is so vanishingly rare that it seems obnoxious to force a try/catch
It would be simpler to just use
// @ts-expect-errorthan introducing a whole new error handling concept.How would you specify the error type?
// @ts-expect-errorwould allow you to leave "safe" errors unhandled.// @ts-expect-errorwould allow you to leave "safe" errors unhandled.Yeah, but then you lose all other type-checking for that line.
michaelangeloio commented
on Nov 10, 2023 More actionsI made an extension similar to this!
Feel free to check it out:
https://marketplace.visualstudio.com/items?itemName=michaelangeloio.does-it-throw-vscodeVery cool! Any plans to build for IntelliJ?
Michael Angelo Rivera (@michaelangeloio) Super cool, thanks for sharing! I would also love to see a JetBrains plugin implemented.
Any plans/ideas about a CLI? Could it report the class of error? i.e.
class MyCustomError extends Error {}RyanCavanaugh commented
on Nov 10, 2023 MemberMore actionsLocking for clarity. This issue is concluded and will not be reopened; see #13219 (comment) . For general discussion on the topic, continue if needed at #56365.
- locked as resolved and limited conversation to collaborators
on Nov 10, 2023
The typescript type system is helpful in most cases, but it can’t be utilized when handling exceptions.
For example:
The problem here is two fold (without looking through the code):
In many scenarios these aren't really a problem, but knowing whether a function/method might throw an exception can be very useful in different scenarios, especially when using different libraries.
By introducing (optional) checked exception the type system can be utilized for exception handling.
I know that checked exceptions isn't agreed upon (for example Anders Hejlsberg), but by making it optional (and maybe inferred? more later) then it just adds the opportunity to add more information about the code which can help developers, tools and documentation.
It will also allow a better usage of meaningful custom errors for large big projects.
As all javascript runtime errors are of type Error (or extending types such as
TypeError) the actual type for a function will always betype | Error.The grammar is straightforward, a function definition can end with a throws clause followed by a type:
When catching the exceptions the syntax is the same with the ability to declare the type(s) of the error:
catch(e: string | Error) { ... }Examples:
Here it’s clear that the function can throw an error and that the error will be a string, and so when calling this method the developer (and the compiler/IDE) is aware of it and can handle it better.
So:
Compiles with no errors, but:
Fails to compile because
numberisn'tstring.Control flow and error type inference
Throws
string.Throws
MyError | string.However:
Throws only
MyError.