Skip to content

Lint for undocumented exceptions. #58232

New issue

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

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

Already on GitHub? Sign in to your account

Open
bsutton opened this issue Sep 21, 2020 · 30 comments
Open

Lint for undocumented exceptions. #58232

bsutton opened this issue Sep 21, 2020 · 30 comments
Labels
area-devexp For issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages. devexp-linter Issues with the analyzer's support for the linter package linter-lint-request P3 A lower priority bug or feature request type-enhancement A request for a change that isn't a bug

Comments

@bsutton
Copy link

bsutton commented Sep 21, 2020

The dart language supports exceptions but not checked exceptions.

The lack of check exceptions means that developer must rely on documentation to determine what exceptions a method will throw.

If you look at a significant portions of code published on pub.dev, developers are simply not documenting the exceptions that are thrown.
Documenting exceptions is particularly error prone as it requires the developer to explore each called api to determine the set of exceptions the api throws.

Of course given that most api's are not documenting their exceptions the result is that a developer must examine thousands of lines of code to determine what exceptions are being thrown.
This of course results in the developer not documenting their own api as its simply too much effort and worse they simply are not doing the necessary work to handle exceptions appropriately.
The lack of documentation then cascades up into the next level of code and so on.

The end result is that much of the code published on pub.dev is of questionable quality.

This problem could largely be resolved by providing a lint rule for pedantic that warns if an api throws an exception.

By adding this link to pedantic we would see a very rapid improvement on the quality of code on pub.dev for the following reasons.

  1. as a user of a pub.dev package I can see what exceptions are being thrown and handle them correctly.
  2. as a publisher on pub.dev its now easy to document my exceptions as the linter generates a list of thrown exceptions.
  3. if I make a code change which changes the set of thrown exceptions the linter will notify my to fix the doco.
  4. ide's can be modified to provide a 'quick fix' for this new lint rule which will make keeping doco up to date simple.
  5. with exceptions now documented developers will be force to actually consider what exceptions are going to be thrown and take appropriate action rather than just ignoring them as happens now.
  6. when third party projects change the set of thrown exceptions (as happened recently with firebase) I get a lint rather than having to wait for a runtime exception.

Essentially we end up with a cascade affect and everyone benefits.

Here is an example of how I would see the workflow.

/// Throws NotARealException
void dowork()
{
   actuallyDoTheWork();
}

void actuallyDoTheWork()
{
   throws PosixException();
}

Linter:

The method dowork throws an non-existant exception NotARealException. Please remove.
The method actuallyDoTheWork throws PosixException. Please document the throws statement.

Once I correct the above lints I should then see:

void dowork()
{
   actuallyDoTheWork();
}

/// Throws PosixException when something goes wrong.
void actuallyDoTheWork()
{
   throws PosixException();
}

Lint error:

The method dowork throws PosixException. Please document the throws statement.

I see two alternatives for how the linter inspects the code.

  1. the linter just looks at the body of a method for any direct exceptions and the documentation of all invoked methods (as per the above example)
  2. the linter looks at the body of the method and the body of any direct api calls.

The 2nd alternative would see a much faster improvement in code on pub.dev as it would:

  1. allow users of a library to at least see any 1st level exceptions third party packages throw
  2. would encourage developers to raise issues against the third party project to improve their exception documentation.

The result is that the lint would essentially cascade both 'up' into the developers own code and 'down' into third party projects resulting in an improvement in quality of all dart code.

This lint looks (in my ignorant opinion) fairly easy to implement.

The benefits to the community however will be huge.

@bsutton bsutton added type-enhancement A request for a change that isn't a bug linter-lint-request labels Sep 21, 2020
@bwilkerson
Copy link
Member

Thanks for the suggestion.

If I remember correctly, the original rationale for not supporting checked exceptions was a belief that having any unchecked exceptions effectively negates the value of supporting checked exceptions, and the assumption that unchecked exceptions were unavoidable. I tend to agree, but I mention it not to start a discussion about the value of checked exceptions but because I believe a similar principle is at work here and should be considered.

If we did implement a lint for this, then the lint would only be effective if all of the packages that a given package depends on have also enabled the lint. Packages whose authors did not choose to enable the lint have effectively turned all exceptions into unchecked exceptions. The fact that it requires wide-spread adoption of the lint to give the lint value really does significantly diminish the value of having the lint.

I suspect that you realize that and that that's why you suggested adding the lint to pedantic: as a forcing function for adoption. Unfortunately, adding it to pedantic isn't a trivial process. The pedantic rule set is the set of lints that are used by internal developers at Google. In order to add a lint to that set we'd first need to convince internal developers to adopt the lint, and that doesn't seem like a likely event. So, I don't think we can force adoption that way. Adoption would need to happen more organically.

In the absence of widespread demand for the lint, I don't see much value in adding it. I'll leave this issue open for now so that community members can voice their opinions; if there's enough demand then we'd certainly consider adding it.

@bsutton
Copy link
Author

bsutton commented Sep 23, 2020 via email

@pq
Copy link
Member

pq commented Sep 23, 2020

Thanks for the thoughtful conversation!

I see where Brian is going with the potential limitations if such an analysis were not universally enforced but I'm pretty compelled by @bsutton's observations and the general motivation. If nothing else, it seems like this could improve package documentation.

I'm in favor.

👍

@lrhn
Copy link
Member

lrhn commented Oct 13, 2020

If the analyzer can actually analyze which methods throw which exceptions, then it would be possible to only warn about undocumented exceptions on API boundaries.

  • If your package exposes a method or function which might throw FooException, but isn't documented as such (it's immediate or inherited documentation doesn't have a paragraph starting with Throws which contains [FooException] or FooException), then the lint would warn.
  • If your package uses a method or function from another package which might throw a FooException, and that method doesn't document it, even if they haven't enabled the lint, we can give a hint at the use-site that this method might throw a FooException, at least until it's handled in some way.
  • If a virtual method implementation throws an exception, and it's super-interface signature isn't documented as doing the same, then we should warn that it's a potentially incompatible override. Someone calling the method at the super-interface type won't know that there might be an exception.

I wouldn't worry about code that is internal to a package, only documentation on API boundaries is really important.
Otherwise we force people to add documentation to internal implementation details, where the author is the only one using it anyway. That rabbit hole is what makes people dislike Java's checked exceptions (or to catch and rethrow as unchecked to make it through your helper functions, then maybe recatch and rethrow as checked again at the API boundary).
Alternatively, have a second lint for internal consistency as well, for people who want to document all their internal functions.

The big problem here is obviously correctly detecting which exceptions can be thrown. Static functions are probably reasonably easy, but virtual methods are harder. With the third item above, it might be sufficient to look at the actual interface documentation. It's too much of a false-positive generator to claim that an interface method throws an exception just because one subclass somewhere does, that one class might just be for testing and won't ever affect the real program.

This can work for async functions too, but it's going to be hard to analyze direct uses of Future.error.

It would require analyzing the entire program, to follow call chains to the end, not just looking at signatures and documentation of what you are calling. If that's too much, then it's true that it only works for exceptions that are already documented as throwing.

@bsutton
Copy link
Author

bsutton commented Oct 13, 2020

I would want to document internal method but can understand that might not be everyone's preference (but I still get to think they are wrong:)

@OlegAlexander
Copy link

Perhaps the lint for internal functions can be optional for those who want it. But the lint for public APIs should arguably be on by default.

@natebosch
Copy link
Member

natebosch commented Dec 10, 2020

Overall I do expect this would be infeasible to handle for a sufficient number of cases as to make it trustworthy.

  • If a method is documented as /// Throws FooException if the Bar is not initialized;, but I know I'm initializing Bar before the call, how do I prove to the analyzer that I don't need to document FooException as an exception my method might throw?
  • How would we treat something in a method body like doSomething(() { mightThrowException(); });. Do I need to document the same exceptions or not?

Adding more in depth structure to DartDoc would be a departure from our current idioms and user expectations. This is still pretty readable, but I would strongly oppose any solutions which lead us towards @throws or something similar.
https://dart.dev/guides/language/effective-dart/documentation#do-use-prose-to-explain-parameters-return-values-and-exceptions

The transitive behavior here has potential implications for evolving packages in the ecosystem - the dependencies present when we analyze a package to produce the lints may not match the dependencies present for a downstream users. This pushes towards a place where we need to model any change in exception as a major version bump (which one could certainly argue is a good thing). As a consequence we could have higher ecosystem churn - if one of my dependencies changes a thrown exception, even if that doesn't impact me directly, I can't have a wide constraint on that dependency, because my documentation is forcibly tied to a single major version of my dependency's documentation.

If we do decide to pursue this, we should treat subtypes of Error as implementation details. It shouldn't matter what error I throw, and callers should never catch it. I would be strongly opposed to a model where change from one error to another needs to be a major version bump.

@bsutton
Copy link
Author

bsutton commented Dec 15, 2020

@natebosch thats for the response

If a method is documented as /// Throws FooException if the Bar is not initialized;, but I know I'm initializing Bar before the call, how do I prove to the analyzer that I don't need to document FooException as an exception my method might throw?

It would seem that this scenario could be handled via the existing 'ignore lint' mechanism.
I don't know if you can pass an argument to the ignore mechanism but this sort of concept might work:

void a(int b)
{
    if (b == null) throw Bad;
}

void c()
{
   // ignore:  undocumented_throws Bad
    a(1);
}

As to the prose I supposed the following might work:

/// If you don't pass [b] then we will throw [Bad].
void a(int b)
{
    if (b == null) throw Bad;
}

Mentioning the exception by name in square brackets could be considered sufficient for the linter to be happy.

The transitive behavior here has potential implications for evolving packages in the ecosystem

I agree that this is needs some thought.

any change in exception as a major version bump (which one could certainly argue is a good thing).

This very issue is what brought me here. Firebase made a breaking change as to what exceptions were being thrown and I had no idea that I had a problem. Fortunately the code didn't make it into production.

As a consequence we could have higher ecosystem churn - if one of my dependencies changes a thrown exception, even if that doesn't impact me directly,

This is really no different to a change to an api that you aren't using.

I doubt that exceptions change any more frequently than apis (in fact I suspect they change less often) as such I wouldn't expect this to create any great amount of churn and the churn it does create will be valid and necessary.

we should treat subtypes of Error as implementation details.

I think I agree with this. If users felt otherwise it could be implemented as an alternate lint.

@bsutton
Copy link
Author

bsutton commented Dec 15, 2020

I think we should also allow documentation to target a base exception class.

class BaseException implements Exception {}

class DerivedException extends BaseException {}

/// There is no escaping the fact that we throw [BaseException]
void main()
{
throws DerivedException();
}

I would consider this lazy documentation but we probably need to cater for everyone.

We could have a lint that controls whether this is allowed or explicit declarations of each exception is required.

Explicit declarations of every exception can become very noisy.

@safield
Copy link

safield commented Jan 14, 2021

This is a pain point for me. Not sure what the best solution is for dart, but I personally am missing my checked exceptions I am used to.

@rubenferreira97
Copy link

rubenferreira97 commented Apr 19, 2021

After some Flutter and Dart experimenting I can feel empathy with this issue, caused mostly by poor documentation. I miss checked exceptions, however I know they don't belong to dart.

Given that I would like to ask if tracing exceptions by analyzing the throw keyword is feasible in Dart Lint or Dart itself.

Example:

We know that functionThatMightThrow throws SomeException

// Linter (Information): This function throws the following exceptions: SomeException.
void functionThatMightThrow()
{
   if(someErrorOccurred) throws SomeException;
}

We know that functionThatThrows throws SomeException and SomeException2 (since this function itself do not catch SomeException)

// Linter (Information): This function throws the following exceptions: SomeException, SomeException2.
// Linter (Warning): Unhandled exceptions on line functionThatMightThrow: SomeException (Will rethrow)
void functionThatThrows()
{
   functionThatMightThrow(); 
   throws SomeException2;
}
//Linter (Warning): Unhandled exceptions on line functionThatThrows: SomeException, SomeException2 (Will rethrow)
void doStuff()
{
   functionThatThrows();
}

This might be an optional rule. Since Dart seems to differentiate between Exceptions and Errors maybe take that into account and only check for throws Exceptions . Exceptions normally need to be catch as they represent run-time problems.

This could be a warning or just an informative list of exceptions that each function throws, like mouse hovering a function and giving the list.

I don't know if this could be ever be achieved however I think that this issue really needs some care or else what will happen is: programmers will not handle exceptions for a foolish reason, they don't know which exceptions to catch. Even if we might not know why those exceptions are being throw, since that's the documentation role, at least we are aware that the program might throw X, Y, Z exception.

@LukasPoque
Copy link

LukasPoque commented Sep 11, 2021

Any updates on this topic? Would love to see something like this as lint rule.

@Levi-Lesches
Copy link

I think with the rules from @lrhn's comment and the ability to use an // ignore comment, this lint would be perfect.

@xuanswe
Copy link

xuanswe commented Dec 26, 2021

Problem

  • We don't want checked exceptions, that's good for me. I had bad experience with checked exceptions in Java.
  • So now, with runtime exceptions, developers always have 2 questions:
    • May this statement throw exception?
    • If yes, which exceptions could be thrown at runtime?

Expectations

Dart tool should answer these 2 questions with the least and acceptable effort in both create-site and use-site.
Ideally, I would like to not having any changes at create-site and only minimal changes per project at use-site.
Performance should be considered too in case of deep transitive dependencies (both project internal and external).

Solutions?

Honestly, I don't have any solution in mind yet. I also see that all proposed solutions above don't satisfy my expectations.
For now, I just want to share my summary about the original problem and continue to think about ideas to satisfy the expectations.

@subzero911
Copy link

subzero911 commented Jan 23, 2022

It's better to use annotations for that.
Annotations are more native Dart way to add some metadata for linter, than comments.

@throws(BadRequestException(), FatalError(), SomethingWentWrongExeption())
abstract class TimeApiService {
  Future<TimeResponse> getTime();
}

@bsutton
Copy link
Author

bsutton commented Jan 23, 2022

@subzero911
The advantage of doing it in the comments is that a lot of libraries already document the exceptions in the comments so we get to leverage that rather than having to duplicate the existing comment.

Example from Version:

  /// Returns the primary version out of [versions].
  ///
  /// This is the highest-numbered stable (non-prerelease) version. If there
  /// are no stable versions, it's just the highest-numbered version.
  ///
  /// If [versions] is empty, throws a [StateError].

@xuanswe
Copy link

xuanswe commented Jan 25, 2022

Regardless using annotation or documentation, we are converting all runtime exceptions to the problem of checked exceptions, which force developers to add something that should be done by compiler or language tools. It only changes from method declaration (in Java) to other places (annotation or documentation). Doesn't make sense to me :-).

To me, compiler or language tools have the responsibility to tell me:

  • hey, you may have exception A, B or C at runtime. You can ignore my warnings and find them again anytime. IDE should show this information when developers ask for with a shortcut keyboard.
  • I couldn't find any documentation for exception A and B, but you can jump to this line in this file to see how the exception could be thrown in the call chain.
  • I found documentation for exception C at this place. For the source code that throws the exception in the call chain, check these lines in these files.

All these jobs should not be on developers' shoulders :-)

@rubenferreira97
Copy link

rubenferreira97 commented Jan 25, 2022

It seems you are mixing lint errors with compiler errors. The developer does not need to add something in order to compile. The only way to tell the developer that he might be forgetting something, well it's telling him "Hey this is a lint error, not a obligatory one, it might be worth to have a look".

Regarding your last sentence, developers should (and IMO must) have documentation on their shoulders. Developers also should check for errors if they are calling another person's code with a respective API. You shouldn't skip their instructions (documentation).

@xuanswe
Copy link

xuanswe commented Jan 25, 2022

@rubenferreira97 sorry for confusion, I think I need to clarify that I am talking only on the use-site (including transitive call chain).

I totally agree that, we need a rule to show message (of course, configurable levels: ignore/info/warn/error) to add documentation of possible exceptions and removing impossible exceptions. But only at the method, where the throws keyword is found directly inside it.

I disagree to force documentation anywhere, which indirectly throws an exception. Because if a method throws an exception indirectly, this method is considered use-site. The use-site developer doesn't have responsibility to document the exception. He has the right to know about it but ignore it. "Ignore" means "rethrow automatically" like what we are doing now.

@xuanswe
Copy link

xuanswe commented Jan 25, 2022

Now, even if we have that rule for the method, that directly throws an exception, we should never believe that the documentation about exceptions provided by a package is the source of truth. The main reason is that, the package could ignore the rule or even doing things wrongly (you know human make mistakes).

If documentation is not the source of truth, dart needs to gather this information using the source code as the truth and merge the description of the valid exception found in the package documentation. It will lead to performance problem. I am not sure if we can have any solution for the performance issue here.
Do you think if indexing could solve this issue? Dart should only re-index for the part, which is detected there are changes. The indexing parts should be prioritized with an optimal ordering, ex. internal methods before methods on dependencies, the deeper dependency should be indexed after the nearer one. The indexing of any part could be available immediately even the whole indexing is not yet completed.

For the packages on pub.dev, the index file could be generated by pub.dev, so localhost can ignore indexing for the parts found from a trusted place like pub.dev.

@Levi-Lesches
Copy link

Now, even if we have that rule for the method, that directly throws an exception, we should never believe that the documentation about exceptions provided by a package is the source of truth.

I disagree with that. Two points often brought up around this topic are:

  • my use-case means this error will not happen (like divide-by-zero errors when you explicitly check for value != 0)
  • I want to document extra information about this error to be shown along with the warning

Both of these are perfectly valid since while the compiler may know what could happen, the programmer (usually) knows what will happen, and why it's happening, and what the real behavior should look like. If you want to ignore that, that's fine, but I wouldn't say that's the popular opinion. After all, Java is one of the only mainstream languages with checked exceptions and the rest of them seem to work just fine.


I disagree to force documentation anywhere, which indirectly throws an exception. Because if a method throws an exception indirectly, this method is considered use-site.

Maybe I'm not following here, but this is where you kind of lose me. Say I have this:

E a<E>(List<E> values, int index) => values[index];
int b(int index) => a([1, 2 ,3], index);
void main() => b(4);

As you can see, this will throw an IndexError at runtime. Question is, where does the documentation go? At first, b was a call site so it's exempt from documenting it. But when you look at it from main's perspective, it's using b which can potentially be unsafe, but b has no documentation. Assume each function is in a separate package with different authors -- who sees what warnings? Here's at least how I'd like to see it:

  • a gets a lint: `Possible IndexError at line X, using List.[], but no corresponding documentation was found
  • a can choose to not document the runtime error using the standard // ignore comment:
E? a<E>(List<E> values, int index) {
  // By checking for bounds, we avoid the error below
  if (index >= values.length || index < 0) return null;
  else return values[index];  // ignore: undocumented_error
}
  • Alternatively, a can choose to document the exception instead. I'd be most happy with the Dart-ish style of just searching for the error in the dartdoc square brackets.
/// We don't want this to be null, so we'd rather throw an error. 
/// 
/// Throws an [IndexError] if the index is out-of-bounds.
E a<E>(List<E> values, int index) => values[index];
  • The author of b can hopefully now see a's error info somewhere. The question is, is b responsible for documenting its usage of a, and the possibility of an [IndexError]? For convenience, we may think "no, the real error is in a, just pass that info along". But that's no better than saying a shouldn't have to document its IndexError because the "real error" is in List.[]. However you square it, b is just as responsible for that error as a is responsible for controlling List's behavior.
  • As you can see, it's not enough to just pass a's documentation along, since whoever uses b (main) doesn't know what the values parameter is -- it's taken care of by b. In other words, we have to document this IndexError in terms of b's inputs and outputs. So I think we should have to do that:
/// Calls [a] with a simple list of ints.
/// 
/// Since this list is so small, this will result in an [IndexError] If [index] is more than 3.
int b(int index) => a([1, 2 ,3], index);
//  ^ LINT: Possible IndexError at line X using `a`, but no corresponding documentation was found.
  • Or, again, we can choose not to document this by handling it ourselves:
/// Call this function with a number smaller than 3.
///
/// Notice how I don't have to call out the error by name here. That's because I'm no longer
/// documenting a runtime error, just the normal behavior of my function.
int b(int index) {
  if (index < 0 || index > 3) return 0;
  else return a([1, 2, 3], index);  // ignore: undocumented_error
}

Now, main will see that it needs to call b with a number smaller than 3, whichever way b decides to handle it. That's the goal, isn't it?

@xuanswe
Copy link

xuanswe commented Jan 26, 2022

Basically, I see that we all don't have controversial on direct exceptions. The controversial is on transitive exceptions.

For transitive exceptions, I think this way:

  • dart will provide the truth about the exception with call chain from source code
  • developers "optionally" provide additional documentation about the "transitive exception" in each step in the middle of the call chain.

So the final result is that, dart will show the truth (real call chain from source code) plus the additional documentation found from the call chain.

2 reasons why I couldn't consider documentation is the source of truth:

  • Current packages are not providing enough information. Many of them are just ignored totally in the documentation.
  • The rule will be optional anyway, meaning that any package could just continue not provide the documentation we need.

Could you please prove that I am wrong with the 2 reasons above? I am happy if someone can prove that, then the community could go on the easier path.

Java initially wants to solve this issue with checked exceptions by forcing declaring or handling "transitive" checked exceptions. And it doesn't work well, that's why modern languages learned the lesson and avoid the similar problem. Moving the place of the rule from method declaration to documentation/annotation for transitive exceptions is not the solution.

@lrhn
Copy link
Member

lrhn commented Jan 26, 2022

To me, it's very important to distinguish errors and exceptions. Errors are like Java's RuntimeException and are unchecked. Checking them programmatically is a complete waste of time, because they won't happen in well written code (and non-well written code will, well, throw to signal that, and take potentially down the entire program, and that's fine).

And, also, all code can throw errors, it just might be StackOverflowError or OutOfMemoryError. Or it may be an IndexError if you use an unvalidated index into a list. The mistake was doing that to begin with, not checking against length first. It wasn't a mistake to not catch that error.

We don't generally document errors being thrown, we instead document restrictions that must be checked to avoid that error, like "the index must be non-negative and less than length". That's what the caller must ensure. It's an error if they forget.

Exceptions are where the caller is expected to catch and handle the exception.
I want exceptions to be documented, at least on the public API of a library/package. What it does internally is an implementation detail, but you can obviously document there too for your own convenience.

The way we've chosen to document exceptions in the platform libraries (decided that around the 1.0 release, some code will predate that) is to have a DartDoc paragraph starting with "Throws":

///
/// Throws [SomeException] ... if something, when something, unless something,
/// meaning something. Maybe suggested responses.

(And one of the reasons we try to avoid saying "Throws [ArgumentError] if argument is bad.", is to save that syntax for exceptions. That's why errors are just written as "Argument must not be bad". Also because we don't consider changing which error is thrown a breaking change.)

@Levi-Lesches
Copy link

Yep, I like that there's that distinction. My hope is that such a lint will make people think not just about what errors they haven't documented, but also about how they can handle it themselves. I tried to show it a bit in my example too, where for each step I either documented it or added in if statements to catch possible errors and document that behavior instead.

@Ap3lsin4k
Copy link

Ap3lsin4k commented May 29, 2023

Adding to @lrhn comment:

Language Exception Terminology Terminology for Uncaught Errors
Dart/Python Exception Error
Java Checked exceptions Unchecked errors
Go Error Panic
Rust Result Panic

I wanted to express my support for the idea of implementing a lint for undocumented exceptions. Properly documenting exceptions in code is crucial for improving code clarity, and maintainability, and reducing potential issues during development.

By enforcing the documentation of exceptions and drawing inspiration from other languages' approaches, we can improve code quality, promote explicit error handling, and enhance the overall reliability of Dart projects.


As for exceptions that never occur, I would use a "panic-like" behavior: terminating the program, or logging an unexpected occurrence. This approach emphasizes the confidence that the exception will not be thrown and helps identify unexpected scenarios during development.

I would like to provide an extreme approach to answer @natebosch's question:

... I know I'm initializing Bar before the cal ....

void c() {
  try {
    a(1);
 } catch (Bad) {
    // Perform panic-like action here, such as terminating the program
    log("this is not expected to happen") 
    exit(1);
  }
}

By doing so, other developers can understand that this particular situation is not expected to happen. That way you don't need to ignore the linter.


In Dart, it can be challenging to determine all possible exceptions that a function might raise without thoroughly reading its documentation or analyzing its implementation.

Having IDE support or a compiler that can automatically identify and suggest exception handling for functions would indeed be helpful. However, traversing all inner calls of all functions to determine potential exceptions can be a computationally expensive process, especially in large codebases.

One possible solution to address this issue is by standardizing exception documentation and making it more explicit in function signatures. By providing clear and standardized documentation about the exceptions that a function may throw, IDEs could potentially offer autocomplete or suggestions for exception handling, enhancing developer productivity and reducing the likelihood of missed exceptions.

Drawing from the experiences of languages like Go and Rust, where the exact exception type is not as critical as ensuring explicit error handling, it may be beneficial to focus on encapsulating error messages within exceptions. This approach allows for flexibility in modifying the error message without breaking client code, as long as the exception type remains the same. In contrast, requiring the specification of exact error types, as seen in Java, can sometimes lead to verbosity and increased complexity.

By adopting practices that prioritize standardized exception documentation and promoting explicit error handling, we can improve the developer experience, code maintainability, and overall code quality.

Thank you for considering this suggestion.

@rrousselGit
Copy link

rrousselGit commented Dec 12, 2023

When considering the annotation idea, instead of an annotation, what about a generic typedef?

// The typedef is No-op, and useful only for the analyzer
typedef Throws<ResultT, ErrorT> = ResultT;

Then instead of:

int fn() {
  throw FormatException();
}

We'd write:

Throws<int, FormatException>  fn() {
  throw FormatException();
}

This doesn't impact the function signature.
It is slightly worse in terms of syntax, but has the benefit of working with function variables:

Throws<int, Error> Function() callback;

As for when throwing multiple types, we can use records:

Throws<int, (FormatException, UnsupportedError)> Function() callback;

This even supports more advances cases, such as a function returning a function:

Throws<Throws<void, FormatException> Function(), StateError> callback;

callback = () {
  if (something) throw StateError();
 
  return () {
    if (something) throw FormatException();
  };
}

@bsutton
Copy link
Author

bsutton commented Dec 12, 2023

@rrousselGit
my inclination is not to go down this path.

We still need to document why the exceptions are being throw so this proposal just ends up in documenting the exceptions twice.

@rrousselGit
Copy link

Making sure exceptions are explicitly thrown or handled is the responsibility of the type system though, not comments.
That's why lots of folks use the Result pattern.

@MisterMirko
Copy link

I would love to see some results rising from all the discussions. Maybe we don't need the full fledged solution that hits it all. Maybe we can start rather simple with a lint that is limited to only scan the body of a function for throw statements and advise the programmer to document the exception.

@bsutton
Copy link
Author

bsutton commented Oct 21, 2024 via email

@devoncarew devoncarew added devexp-linter Issues with the analyzer's support for the linter package legacy-area-analyzer Use area-devexp instead. labels Nov 18, 2024
@devoncarew devoncarew transferred this issue from dart-archive/linter Nov 18, 2024
@pq pq added the P3 A lower priority bug or feature request label Nov 20, 2024
@bwilkerson bwilkerson added area-devexp For issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages. and removed legacy-area-analyzer Use area-devexp instead. labels Feb 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-devexp For issues related to the analysis server, IDE support, linter, `dart fix`, and diagnostic messages. devexp-linter Issues with the analyzer's support for the linter package linter-lint-request P3 A lower priority bug or feature request type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests