Skip to content
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

[dynamic-import-vars] Give default promise rejection timing parity with cases #825

Merged
merged 3 commits into from
Mar 27, 2021

Conversation

bearfriend
Copy link
Contributor

@bearfriend bearfriend commented Mar 3, 2021

Rollup Plugin Name: dynamic-import-vars

This PR contains:

  • bugfix
  • feature
  • refactor
  • documentation
  • other

Are tests included?

  • yes (bugfixes and features will not be merged without tests)
  • no

Breaking Changes?

  • yes (breaking changes will not be merged unless absolutely necessary)
  • no

If yes, then include "BREAKING CHANGES:" in the first commit message body, followed by a description of what is breaking.

List any relevant issue numbers:

Description

The immediately rejected promise in the default clause of the generated switch does not have timing parity with the other clauses, making it impossible to register a catch on the promise before it is rejected, which causes debuggers with "pause on exceptions" (but not "pause on caught exceptions") enabled to always pause in the default clause, even if you have registered a catch on the returned promise. This queues the promise rejection as a microtask to remedy that situation.

@bearfriend bearfriend force-pushed the default-clause branch 2 times, most recently from 044210c to 7652817 Compare March 3, 2021 17:48
@shellscape
Copy link
Collaborator

@LarsDenBakker it looks like this has some community support. Please have a look over this when you have the opportunity.

@LarsDenBakker
Copy link
Contributor

LarsDenBakker commented Mar 10, 2021

This changes the natural behavior of dynamic imports, I'm not sure if it's a good idea.

For example, given:

import(`./pages/${page}.js`);

If the requested file does not exist, the browser will throw an error before bundling with rollup. Why should it not throw an error after bundling with rollup?

If your code is intended to ignore/handle 404s, you can do this:

try {
  await import(`./pages/${page}.js`);
} catch (error) {
  // handle the error
}

That way the code behaves the same both before and after bundling.

@bearfriend
Copy link
Contributor Author

bearfriend commented Mar 10, 2021

Perhaps it should (optionally) throw an error after bundling, but if it does, it should do so asynchronously, as the 404 error would be.

The reason it would be nice to be optional is that in the actual code that is run after bundling, there is no natural error. Nothing has been attempted and nothing has failed. If you loop through a set of variables until one matches, as we do (and one is guaranteed to match), it would be nice not to have to catch a pointless error.

Also note that your example, as written, will never catch as the promise is returned successfully, even if it will be rejected. Even if you await the import, after bundling the error occurs in the scope of the generated function and it will throw.

@LarsDenBakker
Copy link
Contributor

LarsDenBakker commented Mar 10, 2021

You're right I'm missing an await in my example, I updated it.

Improving the way we handle the rejected promise is fine by me, I wasn't aware we didn't achieve this already with the current code.

I'm personally still not convinced of the use case you describe. If there is high demand for it, we can add it. But as far as I can see it's a few lines of code to handle the catch yourself and it makes the code more explicit and work without bundling as well...

@bearfriend
Copy link
Contributor Author

bearfriend commented Mar 10, 2021

I think that's fair. If we queue the promise rejection as a microtask, making the default clause optional becomes less necessary, and it can be handled in a catch much more effectively. I probably should've split the methods into separate PRs to begin with.

Should I go ahead and revert the defaultClause code?

@LarsDenBakker
Copy link
Contributor

Sure, that would be fine!

@bearfriend bearfriend changed the title [dynamic-import-vars] Allow default clause to be disabled and give timing parity with cases [dynamic-import-vars] Give default promise rejection timing parity with cases Mar 10, 2021
throw new Error('Unknown variable dynamic import: ' + path);
return new Promise((resolve, reject) => {
queueMicrotask(() => {
reject(new Error("Unknown variable dynamic import: " + path))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry not reviewed this in detail, but wondering if queueMicrotask is supported in most places or if we should use something a bit older instead to achieve this?

Would a standard return Promise.reject() not work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would not. The actual code falls back to setTimeout.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked in a bit more detail.

What wouldn’t work with return Promise.reject(new Error("Unknown variable dynamic import: " + path)) work?

The return is always a promise then which would resolve/reject async. I understand why the sync throw before was not great

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code actually already is using Promise.reject. The documentation just doesn't match. Please run this with your dev tools open and Pause on exception enabled to see what this fixes.

https://jsfiddle.net/g6u4femL/

Note that even though both promises are caught, it will still break on promiseA.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok thanks for explaining.

Seems strange there’s not a simpler way but 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like this is something dev tools should handle? If you’re pausing on uncaught exceptions I wonder why it doesn’t wait a tick internally until the point when it actually rejects 🤔

Copy link
Member

@tjenkinson tjenkinson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me, but I think we could just return a Promise.reject(...) and solve the problem with less code, so that would be preferable IMO

Copy link
Member

@tjenkinson tjenkinson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry just spotted something else

}
switch (path) {
${paths.map((p) => ` case '${p}': return import('${p}');`).join('\n')}
${` default: return new Promise((resolve, reject) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use a standard function in here vs the arrow one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose an arrow function isn't strictly necessary. Do you see a downside to it in this case? There's no scope it's ignoring.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends what browsers this is targeting. Promises are polyfillable but the arrow syntax isn’t, so it would be safer to stick with the older function format I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok. Didn't realize we were going back that far with browser support, but yeah I guess it's the safest thing to do and no real downside.

@bearfriend
Copy link
Contributor Author

Anything blocking this?

@tjenkinson
Copy link
Member

Looks good to go but @shellscape usually does the merge/release when they get chance :)

@shellscape
Copy link
Collaborator

@tjenkinson put your green checkmark on it and I'll merge it post haste

@shellscape shellscape merged commit c328e2c into rollup:master Mar 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants