-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Rethrowing an AsyncError leads to wrapped AsyncErrors #7781
Comments
If an AsyncError.syncError getter is added, it would be necessary to also have a getter that returns the un-nested stack trace. |
The more I work with code that uses AsyncError, the more I think it's important that the non-AsyncError error be prominently available. Right nwo it's very easy to naively just check .error, which will work in many cases but fail in others. For example, the pkg/unittest code has a collection of "throws*" methods (e.g. "throwsArgumentError") that check whether a Future chain throws an exception of a given type. They test against .error, which means that they'll give false positives for any level of nested AsyncErrors. I think there are two ways to avoid having this pitfall. One would be to never create nested AsyncErrors, which makes life simpler but throws away potentially valuable stack traces. The other is to make AsyncError.error and .stackTrace return the non-AsyncError values, while adding additional getters (.nestedError and .nestedStackTrace?) for the AsyncError chain, or relying on the existing AsyncError.cause getter (I'm not sure what the semantics of that are supposed to be). I hate throwing away stack information, so I'd prefer the latter solution. |
You should never throw an AsyncError (notice that despite its name, it doesn't extend Error), and it should therefore not be nested. Noticing that you effectively rethrow by comparing identity of the thrown object is a good idea, and I think we should do that. I'm even willing to detect that you throw an AsyncError object and consider that a "preprocessed async error" and just forward it unwrapped (so throwing the AsyncError itself would effectively be a rethrow, making my first sentence void). You should not lose stack-traces since we chain AsyncErrors when we throw in exception handlers. Added Accepted label. |
That's a reasonable intent, but I think it's one people will fail to understand given the name. Throwing things whose name ends in "Error" is quite common and users probably can't be relied on to notice that AsyncError doesn't extend Error. I know there's been discussion of relaxing some of the function subtyping rules. If that happens, I wonder if it would be clearer to have something like: class Future { So [error] is always the original unwrapped error. If you want the chain of async failures that cascaded from that, they're in [causes]. Internally, catchError would keep track of [causes] so that if [onError] throws it doesn't have to explicitly hand [causes] back to catchError. I don't generally prefer two objects where one will do, but I wonder if this would work better in the common cases where you just want the "real" error and the other stuff in AsyncError is used for debugging and logging.
I like this. Future now does type-sniffing in then() to see if the result is a Future or not, so I think it's consistent and helpful to do something similar for errors. |
I'd rather have both Let's not assume that we'll make the type system that unsound - then we are better off not using it. |
I'm extremely wary about making the API move in a direction where getting a stack trace along with the error is more difficult or farther from the default path. One of the biggest pain points in dealing with asynchronous code in Dart is that stack traces are often dropped on the floor, making it extremely difficult to find where errors are occurring. There are two main language-level causes for this. First, stack traces aren't attached to exceptions, so they have to be manually passed around alongside the exception object. Second, there's no way to attach a pre-existing stack trace to an exception, so re-throwing always loses stack information (see issue #3974). I really like the fact that AsyncError keeps the stack trace along with the exception; it solves the first cause I mentioned above. If we make it easier to get the underlying exception object without a stack trace, that mitigates this benefit. I had thought that AsyncErrors also solved the inability to throw an exception with a stack trace, but that's not the case if they're not intended to be thrown themselves. Neither Bob's nor Lasse's solution solve this. You can mitigate it somewhat by detecting whether the same exception was re-thrown, but that only solves some cases. I advocate that we allow AsyncError to be thrown and change .error and .stackTrace to unwrap nested chains. This ensures that stack trace information is kept proximate to the exception whenever possible, it allows re-throws with an attached trace, and it minimizes the cases where programmers will unwittingly discard the stack of an exception. |
I'll change it so that if you throw an AsyncError from an event handler, it's used directly, and not wrapped. |
SGTM. |
This has been fixed. If you see throwing an AsyncError from a user-provided function in a Stream-methods causing a wrapped AsyncError, then please file a bug-report. Added Fixed label. |
I started an email thread, but I'm going to file a bug instead so I can workaround it and reference the bug. The original thread:
return someFuture.catchError((e) {
// Rethrow.
throw e;
});
This gives you a new AsyncError containing the original AsyncError (I think as both .cause and .error) containing the original error.
Having the chain of AsyncErrors is nice when there are distinct exception objects at each link. It's confusing when the "exception" is just the same AsyncError object.
This is causing some test failures in pub where we want to catch an async exception and translate it to something nicer if it's of certain types.
In response, Nathan says:
In the old Future API, transformException checked whether the exception being thrown was identical to the exception passed in and kept the original stack trace if so. I propose we do the same sort of re-throw detection with AsyncErrors in catchError and avoid re-wrapping if an exception is re-thrown.
This may not completely alleviate the need to manually unwrap multiple layers of AsyncErrors, though... it's possible that an unrelated AsyncError is thrown in a Future chain that will still cause multiple layers of wrapping. Maybe some sort of AsyncError.syncError could be added that returns the first non-AsyncError error in the error chain?
The text was updated successfully, but these errors were encountered: