-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Importing modules which failed evaluation previously #862
Comments
If you call If that's correct, then what's the analogy for Tangentially, re-throwing feels like the right behavior in a future where we allow synchronous nested try {
import "./b.mjs";
} catch (error) {
if (isReallySerious(error)) {
throw error;
}
}
launchTheMissiles(); |
My implementation would never finish linking if the Module resolved produced an error. It has a ModuleJob structure that keeps a hold of any error it produced. Right now the I am not sure what should happen though on finding an error currently I rethrow, but I think generating a new error everytime makes sense.
https://tc39.github.io/proposal-dynamic-import/#sec-hostresolveimportedmodule Doesn't specify the cache/idempotency behavior of errors from eval, so thats why this is coming up.
Do you mean just It is somewhat equivalent to errors during ModuleDeclarationInstantiation.
That is my current behavior, but people can muck with the error between imports which has made me start to lean towards throwing new Errors every time.
I'm still not sure on those synchronous blocking import declarations, but it is up for discussion. |
I'm not sure what "throwing new Errors every time" would mean. If we're not throwing the original error object, as produced by
@bmeck Thanks, I've updated my terminology to "static |
We are currently looking at this, and we might propose changing the spec to be specific about caching since it is needed for realms. Assuming that we will cache the resolution of |
@caridy this is a result of |
There are many ways to write buggy code and many ways to screw up module initialization that doesn't involve throwing an exception during module initialization. A similar failure would occur if someone coded: //a.mjs
import {okToFire} from "./b.mjs";
okToFire();
launchTheMissiles(); //b.mjs
var initialized=true; //no TDZ, set to undefined during modulation instantiation
export function okToFire() {
if (initialized) {
initialized=false;
throw newError("boo");
}
} Clearly using module initialization success/failure (however it is defined) for control flow is a bad code smell. But trying to define the language in a manner that makes them impossible is a whack-a-mole endeavor. That's |
The way I've tackled this problem in loader implementations has been to remove errored load records and all their parents from the loader registry immediately and synchronously as the error happens. Removing synchronously is important to avoid possible race conditions as well in the error handling. So the behaviour of the example would then be that main2.js fails the first load, then reloads b.mjs for a.mjs from the cache (if using the cache, otherwise refetching), causing the error to fire again, and as a separate second execution. Caching the error is certainly an alternative, but the default thing users want is the ability to "deal with" errors, and not have them causing leftover state which blocks loads from working for the remaining duration of the page (which is especially tricky without a registry API to remove this state). |
I've written up a large, large variety of error cases as part of ironing this out for HTML. This question remains a possible inconsistency, although I'm still not sure about how bad it is:
This seems a little surprising to me still. Again, we could fix it on the HTML side, by caching evaluation errors just like we cache parsing and instantiation errors. But I'm just not sure what the right approach is. |
I agree that it is a little surprising. While I somewhat sympathize with Allen's point above, I don't see it as an argument for the current behavior. |
Agreed that this is bad. @allenwb, the issue isn't that you can screw up initialization, but that it may still non-deterministically "succeed". It shouldn't be possible to execute a module when execution of its (direct or indirect) dependencies has failed. |
There are several different ways things can go wrong with <script type=module>. In order from earliest to latest, for a single module script, they are: - Fetching failures - Parsing failures - Invalid module specifiers - Instantiation failures - Evaluation failures This tweaks the way that these errors interact, to ensure that fetching failures are treated one way, causing the <script>'s "error" event to fire, and the other failures are uniformly treated as errors running the script, causing the global's "error" event to fire. This also makes it clear that when fetching descendant module scripts, you can bail out early if one of them fails to fetch or has previously been discovered to be errored. Evaluation failures are particularly tricky for dependencies, as usually ModuleEvaluation() is a no-op if it has happened previously (either successfully or not). This is discussed in more depth in tc39/ecma262#862. To work around this, we need to store the evaluation error, if one occurs, similar to what we already do for instantiation errors. Fixes #2567.
I've updated whatwg/html#2595 so that in the near future the HTML spec should treat evaluation like all the other failures. I've added an item to the May agenda to have a quick discussion on if we want to mandate this for all hosts, but that's not as urgent as it sounds like browsers and Node.js at least will cache the error. |
@rossberg-chromium I see. Yes, I agree. If ModuleDeclarationInstantiation or ModuleEvaluation of some module fails the first time either is invoked then they should also fail for any subsequent invocations for that module. This probably should be stated in the Table 38 requirements for those abstract Module Record methods. One way to deal in the spec. with failed module evaluation is to make the [[Evaluated]] field of Module Records into a tri-state value (uninitialized/initialized/failedInitialization) and to deal with setting the appropriate state in Source Text ModuleEvaluation. It isn't clear that it is necessary (or desirable) to propagate any actual exception value thrown out of a module body. Prior to supporting dynamic import, any such error would have propagate to being a top level unhanded exception, so the actual exception value wouldn't have been available to the JS code (at least as specified). But dynamic import changes that. I'm concern that using the same exception value on multiple dynamic import invocations could be exploited as a communications channel. For that reason, I think a fresh exception should be propagated each time an attempt is made to initialized an already failedInitialization module record. I suggest that exception should be a SyntaxError exception because it is indicating that the overall structure of the program (as represented by the imported modules) can not be proper constructed. |
I don't think we should be re-generating new exceptions, losing the original context. In HTML at least it's important for telemetry purposes to get the same exception so that people can de-dupe to a single root cause. So we plan to catch any failures and store them for propagation to the top level and/or to dynamic import(). |
An user thrown exception in a module body can carry an arbitrarily complex user provided payload. Seems risky to hang on to such things and pass them back out of dynamic import() multiple times. |
I would argue this falls under similar Idempotency goals as HostResolveImportedModule . Re-using the same value can be both seen as a pro and a con, but I think consistency across existing operations can be seen as valuable. |
@allenwb How is that riskier than the way that modules typically export things like functions and classes, which, unless frozen, could be used to hang off an arbitrary payload (not to mention mutable export bindings)? You can freeze the functions you export the same way as you can freeze the exceptions you throw. |
As we explore trying to spec HTML integration it's becoming increasingly clear that there's a problem at this interface. E.g. in whatwg/html#1545 long ago @jonco3 and I arrived at the conclusion that we needed to do a bottom-up instantiation so that we could properly figure out what caused an error and remember that state, so that future calls to ModuleDeclarationInstantion() don't succeed silently and thus break the idempotence requirements. As you can see this bug is very similar to the one in the OP. But this has caused us some pain now for cicular modular graphs, since we are not doing the simple top-down instantiation: whatwg/html#2629 Looking through this problem, @GeorgNeis and I tentatively think the right solution might be for ES to handle this error propagation which HTML is currently doing manually---both for instantiation and for evaluation. Alternately, we could add host-configurable hooks after each instantiation/evaluation that allows the host to sync state appropriately; this might be more flexible and even necessary. Our current plan is to prototype these approaches, both in spec-land in in V8 + Chrome code, and see whether they solve all of our outstanding problems. If so I will provide some pull requests on the ES-side modifications that would help us out. (Either just one PR with host-configurable hooks, if those prove necessary, or the choice between a PR with host-configurable hooks and one with baked-in error propagation.) Will report back!! |
There are several different ways things can go wrong with <script type=module>. In order from earliest to latest, for a single module script, they are: - Fetching failures - Parsing failures - Invalid module specifiers - Instantiation failures - Evaluation failures This tweaks the way that these errors interact, to ensure that fetching failures are treated one way, causing the <script>'s "error" event to fire, and the other failures are uniformly treated as errors running the script, causing the global's "error" event to fire. This also makes it clear that when fetching descendant module scripts, you can bail out early if one of them fails to fetch or has previously been discovered to be errored. Evaluation failures are particularly tricky for dependencies, as usually ModuleEvaluation() is a no-op if it has happened previously (either successfully or not). This is discussed in more depth in tc39/ecma262#862. To work around this, we need to store the evaluation error, if one occurs, similar to what we already do for instantiation errors. Fixes #2567. However, there are still problems with this setup, which may need further infrastructure changes; see: - #2595 (comment) - #2629 - #2630 But for now the improvement given by this commit is enough to merge it.
This addresses tc39#862 by ensuring that repeated calls to ModuleDeclarationInstantiation() and ModuleEvaluation(), for Source Text Module Records, rethrow any exceptions they previously threw, instead of silently succeeding. This allows host environments to do top-down instantiation/evaluation of module graphs, instead of having to do bottom-up instantiation/evaluation in order to record individual failures and thus prevent future instantiation/evaluation. In the process, this helps formalize some of the invariants previously stated in a vague way, such as "ModuleDeclarationInstantiation must have completed successfully", replacing them instead with an explicit [[Status]] field whose contents can be asserted against. For background on the trouble caused by the previous approach of silent success, see: - whatwg/html#1545 - tc39#862 - whatwg/html#2629
This addresses tc39#862 by ensuring that repeated calls to ModuleDeclarationInstantiation() and ModuleEvaluation(), for Source Text Module Records, rethrow any exceptions they previously threw, instead of silently succeeding. This allows host environments to do top-down instantiation/evaluation of module graphs, instead of having to do bottom-up instantiation/evaluation in order to record individual failures and thus prevent future instantiation/evaluation. In the process, this helps formalize some of the invariants previously stated in a vague way, such as "ModuleDeclarationInstantiation must have completed successfully", replacing them instead with an explicit [[Status]] field whose contents can be asserted against. For background on the trouble caused by the previous approach of silent success, see: - whatwg/html#1545 - tc39#862 - whatwg/html#2629
There are several different ways things can go wrong with <script type=module>. In order from earliest to latest, for a single module script, they are: - Fetching failures - Parsing failures - Invalid module specifiers - Instantiation failures - Evaluation failures This tweaks the way that these errors interact, to ensure that fetching failures are treated one way, causing the <script>'s "error" event to fire, and the other failures are uniformly treated as errors running the script, causing the global's "error" event to fire. This also makes it clear that when fetching descendant module scripts, you can bail out early if one of them fails to fetch or has previously been discovered to be errored. Evaluation failures are particularly tricky for dependencies, as usually ModuleEvaluation() is a no-op if it has happened previously (either successfully or not). This is discussed in more depth in tc39/ecma262#862. To work around this, we need to store the evaluation error, if one occurs, similar to what we already do for instantiation errors. Fixes whatwg#2567. However, there are still problems with this setup, which may need further infrastructure changes; see: - whatwg#2595 (comment) - whatwg#2629 - whatwg#2630 But for now the improvement given by this commit is enough to merge it.
@GeorgNeis pointed out an interesting case. The setup:
Then consider these two different scenarios using these modules:
As currently specced, main1.js will not launch any missiles. But, main2.js will launch the missiles. (This is a mostly host-agnostic statement, but does require a host to obey some basic assumptions about HostResolveImportedModule, so that the same module record is used for b.mjs in both cases.)
This is because the module record corresponding to b.mjs will be evaluated twice: once by the
import()
, and then second by theimport
declaration. The second time, ModuleEvaluation() will be a no-op per the ES spec, so the missiles will launch.This is somewhat surprising, especially because if we replaced the contents of
b.mjs
with a syntax error, we would get the error at instantiation time, which is kind of per spec required to be "cached" (or at least prevent future evaluation):(inside ModuleEvaluation)
Another way it is surprising was stated by @ajklein:
Is this a problem? I am leaning toward yes.
If we agree it's a problem, hosts could fix it by maintaining their own cache of module evaluation results. But as shown above, it's a fairly host-agnostic issue, so I was thinking maybe it should be solved in ES.
The text was updated successfully, but these errors were encountered: