Skip to content

Using Plot with Webpack can lead to ReferenceErrors ("temporal dead zone") in production.  #659

@parkerziegler

Description

@parkerziegler

Hi folks! Thanks for all of your hard work on Plot, I've really been enjoying using the library and learning more about the philosophy behind its design! This bug report comes from an issue I encountered while building a small Next.js application using some of the APIs from Plot.

Minimal Reproduction

To make the bug easier to see, I created a minimal reproduction. The README has instructions for how to run the project and see the errors; getting going just involves running two yarn commands. Let me know if anything is unclear in how to get the reproduction working.

Bug Description

When serving a production build of the minimal reproduction above, we hit the following runtime error:

ReferenceError: can't access lexical declaration 'd' before initialization (Firefox)
ReferenceError: Cannot access 'd' before initialization (Chrome)

After doing some digging using production source maps, I believe the core issue is that Plot is trying to extend from the Mark class in facet.js before Mark has been declared in the built Webpack chunk.

Running a development build of this project does not throw any runtime errors.

Stack Trace

The stack trace of the reported error is as follows (Chrome):

ReferenceError: Cannot access 'd' before initialization
    at Object.vc (497-35515fda497bb06a.js:1:665)
    at Object.27 (facet.js:13:21)
    at r (bootstrap:21:23)
    at Object.2186 (497-35515fda497bb06a.js:1:1422)
    at r (bootstrap:21:23)
    at Object.3962 (histogram-6827f696cfad69e9.js:1:297)
    at r (bootstrap:21:23)
    at (index):5:16
    at route-loader.js:236:51

Following the stack trace is a bit difficult since the production build is minified. However, I did turn on source maps using productionBrowserSourceMaps: true in next.config.js in the reproduction. This is what allows us to trace that the error arose from L13, C21 in facet.js.

What does the stack trace suggest?

L13, C21 of facet.js pertains to the Mark class imported from mark.js. The issue appears to be that, at the time class Facet is being initialized to extend Mark, Mark itself has not yet been initialized. This is what leads to the ReferenceError. d in the minified build pertains to class Mark.

These types of ReferenceErrors in production Webpack builds tend, based on this comment from Webpack's core author Tobias Koppers, to be caused by cyclic dependencies.

Discussion

Does @observablehq/plot have cyclic dependencies?

Yes. Adding the no-cycle rule from eslint-plugin-import to @observablehq/plot resulted in 52 detected cycles.

Lint errors in @observablehq/plot after adding no-cycle.

To be clear, cyclic dependencies are allowed by the ESM spec; they can just (under some circumstances) lead to these kinds of temporal deadzone errors in Webpack. In addition, tree shaking in Webpack makes this even more tricky to track down, because, depending on which parts of the Plot API you use, the chunk Webpack generates for @observablehq/plot will resolve these cycles in different orders. This means you may use a set of APIs that result in a perfectly fine production build with no runtime errors.

What could be done to alleviate this error?

The solution with the highest probability of success would be to eliminate all cycles from the source. However, this is likely prohibitively expensive due to how many cycles currently exist and the challenges associated with such large-scale refactoring.

The specific error here appears to be the result of the following cycle:

  • facet.js imports Mark from mark.js
  • mark.js imports plot from plot.js
  • plot.js imports facets from facets.js

Resolving this cycle could help to alleviate this problem, but I'm not fully certain. It appears to be the most likely candidate based on the stack trace, but I have noticed that this error appears to be mark type dependent. For example, in another project where I use the Plot.barY and Plot.dot marks APIs in the same project, I don't encounter this error. This suggests that the troublesome cycle may be related to some of the transforms APIs (e.g. binX, normalizeX) that the histogram and strip plots use.

Additional Snags

One interesting note is that, if you remove either of the charts in the minimal reproduction (e.g. by deleting histogram.js or strip-plot.js), the error disappears on the page that is left behind. I'm still trying to understand how this affects the resolution of cycles in the built chunk that includes the Plot source.

Final Thoughts

I recognize this may not be a high priority fix for the team, and that using Plot in Webpack-based applications in general may be outside of the core use case of the library. It also may well be that this bug is better directed to the Next or Webpack teams to address; I'm happy to escalate it up to them if you all agree. I mostly opened this bug because I spent a fair bit of time (out of my own curiosity) hunting down its origins myself and wanted to share in case it's helpful to the team, or in the event that someone else encounters this same issue. I'd be happy to try to come up with a draft solution as well, it may just be quite slow with the semester about to get underway for me.

Again, thank you for all the hard work on Plot and I hope to keep contributing!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions