Debugging Electron: A Journey

Debugging is an inescapable part of developing. Though the stereotypical image of programmers emphasizes brilliant innovation (ok, the stereotype is really more marathons of pizza and soda, but grand intellectual conquest is also a big part of it), the reality is that much of our job is actually painstaking detective work: sleuthing through dense lines of code to find the one error that is causing a program to crash. I may be dating myself a bit when I think back to Dennis Nedry’s two million lines of code in Jurassic Park, but even in a far smaller codebase, debugging takes time and effort. This is especially the case when one is learning a new framework such as Electron, which is itself still an emerging technology.

As I noted in my introduction to Electron, the technology allows a developer to write Windows, macOS, and Linux desktop apps using JavaScript, HTML, and CSS, which it compiles into native code. This is a great convenience, but it also has some downsides. As we (my colleagues Ian Lancaster and Alex Pilewski and I) discovered recently, as we neared completion of a productivity app that we are building called Fired Up, one of these is that the high level of abstraction sometimes makes debugging especially difficult. Did the error come from code that we wrote, an npm package, an Apple Script, Windows code…? This was at the heart of today’s featured bug.

As detailed in my prior article, our app is a productivity tool that monitors a user’s usage of desktop applications. It consists of a menu bar window with this data and a UI allowing the user to control and view sessions. Our bug stemmed from the fact that whenever a user closed a program (other than Fired Up itself), our program would crash and error messages would repeatedly flash. These messages were of the supremely unhelpful variety that programmers hate: “JavaScript error: undefined: undefined.” A second error message proved more helpful, as it pointed us toward a specific function, but we were still scratching our heads for a long time.

We first looked at the function in question, which was in charge of sending an object representing the most recent user session to the renderer (see my prior post for my discussion of renderer versus main in Electron). We scoured our main file for any lapses in logic, typos, and the like, but as much as we tried, we could not pinpoint the problem.

Our only breadcrumb, for a while, pointed us toward the window containing the app. Electron has a built-in BrowserWindow object that provides the app’s window(s), and an npm library that we were pulling in, menubar, provides a wrapper for BrowserWindow. Since our main error was telling us that our variable pointing to menubar was undefined, our first move was to add conditionals to relevant functions telling them to only execute pertinent code if the menubar/BrowserWindow exists. We were hopeful that this would solve the problem, but alas, it did not. The error message kept triggering regardless of how we wrapped our functions.

We then scoured the internet for any docs and documented issues related to Electron, menubar, and active-window, another library that we were pulling in. This took a long time to bear fruit, but Ian eventually discovered that an Apple Script embedded in the active-window code might be the culprit. On further inspection, we found that when a user closes an application window, the Apple Script keeps throwing an error. Not being well-versed in Apple Script, we tried to figure out a way around writing it, and we eventually hit upon a work-around: simply wrap the JavaScript code that invokes the Apple Script error message in the active-window module’s index.js file in a conditional that ignores the contents if a user is on a Mac. Our code reads like so:

if (process.platform == 'linux' || process.platform == 'win32') {
throw stderr.toString();

Et voila! No error. Our code even works on Linux without any additional shims needed. We still need to test out our code on Windows, but this work-around solved our vexing problem for the Mac.

Our app works in Ubuntu!

We did, however, encounter one final problem: how to add the modified active-window code to the application, since node-modules are not (and should not be) pushed up to our repo on Github. We discovered that we could move the active-window code containing the modification into a vendors file in our application (which is not gitignored), which we then required into our main file. This trick will surely prove useful when we need to modify code from an npm package for a future application.

We learned a lot from this debugging experience. Debugging Electron, it turns out, may require more steps than some other frameworks in some cases, but with some digging and perseverance, one can still resolve the problem.