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
Consider giving this a new type of error. #74
Comments
This issue is to track part of an existing discussion here. |
I suppose the argument against this is that it would be up to the user to make all their resources' return methods in a custom That's a lot though, and won't work well with any library or language feature which attempts to allow Java-style catching of errors by type, something which I think will be more popular once there is a sane and supported way to rethrow certain errors. |
JS doesn't have a large number of different errors in general. Most errors are just TypeError with a few others speckled in-between. However, @waldemarhorwat did bring up during plenary that // case 1
using const a = ...;
using const b = ...;
// case 2
using const a = ...;
{
using const b = ...;
} In case 1, if both
In case 2, you'll get an
In case 1, the single We could possibly address the "jagged error" concerns by introducing something like Another approach could be to introduce a generic
For alternative (a), such an API could also have something like a try {
await criticalActionUsingResources()
} catch (e) {
if (e instanceof SuppressedError) {
e = e.rootCause();
}
e; // e is now the underlying cause of the error*
} However, this could also become confusing. What do we throw when there's no error from the body? Do we throw a Another alternative could be introduce something like an ObjectDisposedError {
cause: Error, // from the body
suppressed: [
Error, // from b.dispose
Error // from a.dispose
]
} In this case, Honestly, I'm not sure which I prefer more:
|
There is nothing wrong or confusing about an AggregateError that contains, in its |
During plenary, @waldemarhorwat argued that this scenario is inconsistent: // case 1
using const a = ...;
using const b = ...;
// case 2
using const a = ...;
{
using const b = ...;
} I'm not sure there's a consistent way to handle this. Blindly flattening Also, as is mentioned in the OP, there's no way to differentiate between an |
I don't see how it's inconsistent. They're different cases. I agree it's reasonable to provide a way to determine that an error came from disposal - but I don't think there's any need to determine which disposal it came from. Assuming a disposal tag exists, if you wrote the code nested like that, you can write it in a way to tease apart the causes. |
@ljharb: The problem is that this frustrates trivial code refactorings or modifications by making them change program meaning in a dangerously subtle way. The structure of nested errors shouldn't depend on what happens to be in the same local block in a program, only on the order in which the |
Forcing flattening would frustrate nontrivial intentional scope nesting. I'm not sure what you mean by "make all exceptions jagged", though. |
Have you considered not using If that option is not on the table, I'd suggest a new variant c) of "always jagged":
Imo, a disposer should generally not throw, but if it does, that error should suppress any previous error. Not the other way round. This would lead to consistent error structures regardless of refactoring (i.e. adding/removing function calls and/or blocks). |
That is not unlike how Python's // example 1: Exception thrown by MyService is aggregated
class MyService {
[Symbol.dispose](cause) {
...
throw new Error("Failed to destroy resource", { cause });
}
}
try {
using const svc = new MyService();
throw new Error("foo");
}
catch (e) {
// e is AggregateError {
// cause: Error("foo"),
// errors: [
// Error("Failed to destroy resource") {
// cause: Error("foo")
// }
// ]
// }
}
// example 2: Exception thrown by body is logged and rethrown.
class Logger {
constructor(name) {
this.name = name;
console.log(`enter: ${this.name}`);
}
[Symbol.dispose](cause) {
console.log(`exit: ${this.name}${ cause ? ` with error: ${cause}` : ""}`);
return false; // return any non-truthy result (`false`, `""`, `null`, `undefined`, etc.)
}
}
function f() {
using const void = new Logger("f");
throw new Error("foo");
}
try {
f();
// prints:
// enter: f
// exit: f with error Error: foo
}
catch (e) {
// e is Error("foo")
}
// example 3: Exception thrown by body is possibly suppressed.
// NOTE: similar to https://docs.python.org/3/library/contextlib.html#contextlib.suppress
class Suppress {
constructor(filter) {
this.filter = filter;
}
[Symbol.dispose](cause) {
// return `true` (or anything truthy) to suppress the original error
return cause && this.filter(cause);
}
}
try {
using const void = new Suppress(e => e instanceof TypeError);
throw new TypeError();
}
catch (e) {
// not executed
} There is an interesting aspect to this approach, but there are some issues that would need to be resolved:
While powerful, I'm not certain I'm comfortable with the semantics of user-controlled error suppression.
Others have argued against suppressing errors, or at least argued against making the suppressed errors unreachable. I am not certain if we can reach Stage 3 without some mechanism to address this.
The intent behind
While less convenient, we could still use AggregateError {
cause: Error, // from a.dispose
errors: [
AggregateError {
cause: Error, // from b.dispose
errors: [
Error // from the body
]
}
]
} |
We would probably not add any special logic here. We would either optimistically assume that every statement after a In other words, |
The only way I could see it working is if we had a way to unambiguously know a disposable has suppression capabilities. The easiest way to do that would be with a different global symbol (i.e, class BasicDisposable {
[Symbol.dispose](): void {
// cleanup, but no suppression
}
}
class Suppress {
#filter: (error: unknown) => boolean;
constructor(filter: (error: unknown) => boolean) {
this.#filter = filter;
}
[Symbol.exitContext](completion: "return" | "throw", cause: unknown) {
// custom error suppression semantics, all bets are off for CFA
}
}
{
using const res1 = new BasicDisposable();
// res1 has `Symbol.disposable`, CFA can be optimistic
}
{
using const res2 = new Suppress(() => ...);
// res2 has `Symbol.exitContext`, CFA should be pessimistic
} For NOTE: |
I'd like to not repeat the iterator close semantics which suppress closing errors, and make it impossible to report those. It can't even be reported manually (e.g. unhandled rejection or explicit console.log) because the |
Yes. Did I not mention that? I suppose it was mentioned in #49… The only thing I don't like about Python is that it passes a
Oops, yes I spent no thought on that, would be a bad idea to have that as the default. Requiring explicit
Yes, that's clear, but I disagree with the exception from the body being the "(root) cause" of the aggregated error. Notice that my variant c) has the same nesting as variant a), but with the property names Using an
Originally that line was only meant to argue about the inner error being try {
using void = new Disposer(() => { throw new Error("dispose"); });
throw new Error("block");
} catch(e) {
console.log(e.message) // dispose, the "block" is lost
} This would be consistent with how exceptions work in
Yes. The simple approach would be to always have it suppress the previous error. A good disposer shouldn't throw exceptions, and if it does then that needs to be fixed first. A disposer that legimately throws exceptions should check whether there was an earlier error, and decide to ignore, forward, assume as
I don't think they would need to. Most code I've seen in Python that does this, does only suppress some errors (or only under specific conditions) but never all. And since TypeScript doesn't have checked exceptions (yet) anyway, it should just assume that a block using a disposer may always throw an exception or not.
I don't think a disposer should be able to suppress a |
I'd still prefer that scenario just be handled by a different method (i.e.,
I concur with @ljharb that we should just continue to leverage
As currently specified, the use of try {
using x = { [Symbol.dispose]() { throw new Error("dispose"); } };
throw new Error("block");
} catch (e) {
const block = e instanceof AggregateError ? e.cause : e;
const dispose = e instanceof AggregateError ? e.errors[0] : undefined;
console.log(block.message); // "block"
console.log(dispose?.message); // "dispose"
} The wordiness here could also be potentially mitigated by pattern matching and
As mentioned, I'm not sure I'd want the overhead associated with passing the suppressed error to every call to
Perhaps not, but function f() {
outer: try {
return 1;
}
finally {
break outer;
}
console.log("hello");
}
f(); // prints: hello |
I want to share a use case about error management. I am thinking about using it in the context of a SQL transaction in this way: using transaction = {
...
[Symbol.dispose](error) {
if (error) {
this.rollback();
} else {
this.commit();
}
}
}; Or maybe in this way if the use of Symbol.exitContext is retained in the spec. using transaction = {
...
[Symbol.exitContext](completion, cause) {
this.completion = completion;
this.cause = cause;
}
[Symbol.dispose]() {
if (this.completion === 'return') {
this.commit();
} else if (this.completion === 'throw') {
this.rollback();
}
}
}; (It implies Symbol.exitContext method needs to be called before Symbol.dispose method.) I think the lack of error context could be problematic for this particular use case. |
@nicolashenry I'd like to move any further discussion around error handling to #49, so I've copied your comment to that issue. |
Per the September 2022 plenary discussion, we will continue to use |
That looks like a reasonable solution to me. |
Currently I understand that failures in resource cleanup will be thrown as an
AggregateError
, which may or may not have acause
property depending on whether or not the block which uses the resources errored.I would like to suggest that there be a
ResourceDisposalError
or some new type of error, because developers will sometimes want to handle cleanup errors. It may be that I use a particular resource for which I know it is possible for disposal to fail, and I know that I don't care if it does. If so the current spec forces me to write:The text was updated successfully, but these errors were encountered: