Death by Proxy | Hendrik Erz

Death by Proxy

Happy New Year everyone! Let me kick off this year on the blog with a piece on something that has recently caused me some headaches. This new thing is called “Proxy” and, while it is a pretty fancy new way of handling your data in JavaScript, it can make you sad real quick. It is one of these things that can cause very exotic and disturbing looking errors which don’t make sense at first.


Over Christmas, when I had a few days off work, I used that time to do some maintenance work on my app Zettlr. For the uninitiated: Zettlr is a Markdown editor that I and many more people use daily to write everything from short journals to full-fledged research articles. A few people have even written books using it!

Since I’m not a professional developer and have only limited time after work to work on the app, I had to make it simple. That means: I am using Electron for the job and thus Zettlr is built on web-technologies. The Electron-framework is used by quite a lot of day-to-day apps such as Slack, Skype, or Microsoft Teams.

To make my life even easier, I also began using a framework called Vue.js. Vue.js basically includes a lot of functionality one regularly needs when writing a somewhat bigger web app.

Vue’s Reactivity System

Among other benefits, Vue.js uses a reactivity system which automatically tracks any changes in the application state for me. This means: if a user can click a button to show or hide an element I can just write “if the show property is true display this component” and Vue.js takes care that all the required steps (such as actually showing the element) are executed without me having to implement all of that.

Vue.js’s reactivity system – while Vue was still version 2 — was based on JavaScript’s getters and setters. Getters and setters basically allow to intercept the setting of a property. So you simply set some property (e.g., show = true), but in the background a function is called (e.g. setShow(true)). Here is a rudimentary example (you can copy and paste the code into your browser’s developer console to try it out):

var myState = {
    _internalVar: 2,
    set myVariable (value) {
        console.log("Changing myVariable to: " + value)
        this._internalVar = value
    },
    get myVariable () {
        console.log("Someone read myVariable!")
        return this._internalVar
    }
}

myState.myVariable = 123
// Should log: Changing myVariable to: 123

console.log(myState.myVariable)
// Should log: Someone read myVariable!

This is pure syntactic sugar, but it makes the code look more expressive. Instead of calling a litany of different functions, you can just set a property. This means it becomes easier for me as a user of the framework to implement whatever I need for Zettlr, and Vue hides a lot of the actual functionality behind the curtains.

If I set some property show to true, for example, Vue could execute some internal functions that informed other parts of the application that this property has now changed, and have those other parts react to this change in value – hence, reactivity.

For our purposes, it is important to note that these getters and setters could be easily removed again and that they have only a small impact on the objects that had been “decorated” like that.

Introduction to Proxies

Version 3 of Vue now exchanged this system with a newer JavaScript feature called Proxies.

A Proxy basically takes getters and setters to next level: Instead of manually having to implement one getter and one setter per property, whenever you read/set anything on a proxy, a handler is called. That handler is called a “trap” because it can even prevent reading a property (return undefined instead) or writing it. Here’s above’s example using Proxies:

const myState = {
  myVariable: 2
}

const handler = {
  set: function (target, prop, value) {
    console.log("Changing " + prop + " to: " + value)
    target[prop] = value
  },
  get: function (target, prop) {
    console.log("Someone read " + prop + "!")
    return target[prop]
  }
}

const proxiedState = new Proxy(myState, handler)

proxiedState.myVariable = 123
console.log(proxiedState.myVariable)

On the outset, this has benefits for both the developers of Vue and me. I can continue to access properties and Vue always knows what has changed.

However, Proxies are “exotic” objects. That means that they feel like the original object, but with some sort of “event listeners” attached so that the system that proxied the object is notified whenever someone accesses the object.

Proxies are also syntactic sugar and work perfectly fine when used within their intended context. But as soon as you rip them out of their context, proxies can become gruesome monstrosities that break your code in a thousand ways.

Ambushed by Proxies

Specifically, when I migrated from Vue 2 to Vue 3, I did not only face the expected problems of migrating some code (replacing old function names with new ones, etc.), but two more problems: I suddenly saw weird errors and the whole editor acted up. Vue 3’s new reactivity system broke two specific parts of my application: Sending data across IPC channels and the main feature of the app, writing text.

IPC is short for Inter Process Communication and is a way for two separate processes on your computer to share data: one process writes the data into a file or a socket, the other then reads it.

Electron, which Zettlr uses, implements a Client-Server architecture. This means: The server (called “main process”) has the task of central state management, while the clients (called “renderer process”) offer a graphical user interface for the user to actually interact with the app. Since renderer processes cannot communicate with each other, all data has to be managed by the main process.

While the main process runs in the background, each renderer process is represented by a window which you can see. And every window you see actually displays a website with HTML, CSS, and JavaScript to you. That is: Within each window, Zettlr uses Vue.js.

So when the user opens, e.g., the preferences window, the configuration is sent from the main process to the preferences window and is then displayed by Vue.js. When the user then changes an option, it needs to be sent back to the main process which can then actually update the configuration.

However, since those preferences are now inside the reactivity setup of Vue 3 – read: they are Proxies –, we can’t send them simply back over IPC. If we attempt to do so, we will see a cryptic error: “An object could not be cloned”.

The solution in this case was relatively simple: Always de-proxy the data before sending it over the IPC channel.

Getting Rid of Proxies

But how do you do this? For primitives (such as strings, booleans, or integers) this is actually easy: Since their values are always just copied, they automatically get de-proxied as soon as the IPC module reads the data. This is why not the entire app broke, just those parts where I send more complex data.

When it comes to complex data structures (such as objects, arrays, or sets and maps), JavaScript uses pointers like other programming languages. That means: When the IPC module reads the data, what gets copied is simply the pointer, but the object it references is still a proxy. And that is when the IPC module will fail at actually sending the data. So how can we de-proxy those data structures?

A simple list can be de-proxied utilizing the map-method to manually create a new list and read every primitive value inside the list. The primitive values are copied, and the map function creates a new list that contains these copied values. And, since we didn’t proxy that new list, we can safely send it over the IPC channel.

It works similar with lists of simple objects. For example, the tags in Zettlr have only three properties – a color, a description, and the actual tag. So within the map function we can simply construct a new object and read the three primitive properties of the proxied objects.

However, this approach doesn’t work with arbitrary complex data structures. In that case, I have found that simply converting the object to a JSON-string and then parsing the string again works fine. You might run into the problem that some features of JavaScript cannot be represented in JSON and thus will get lost. Also, circular structures (where a nested property references its own parent) will throw an error.

So while this should in principle work fine with data in your Vue components, you should tread carefully and only use this “shortcut” if you are absolutely certain you don’t have circular structures or any data you don’t want to lose.

A mixture of these strategies finally solved the problem of sending data over IPC and many parts of the app worked as expected again. Here are the strategies as code examples:

let simpleList = ['Some string', 412, true]

simpleList.map(element => element)

let moderateList = [
    { prop1: "Hello", prop2: "World" },
    { prop1: "Something", prop2: "else" },
    // ... more of the same objects
]

moderateList.map(element => {
    return {
        prop1: element.prop1,
        prop2: element.prop2
    }
})

let complexObject = {
    prop1: "Some value",
    prop2: 1452,
    prop3: [true, false, false, true],
    prop4: { subprop1: true, subprop2: [] }
}

JSON.parse(JSON.stringify(complexObject))

It’s side effects all the way

However, there was a second problem: After the Vue 3 migration, CodeMirror began to throw weird errors from places that I have never seen before. CodeMirror is the library that enables Zettlr to use syntax highlighting and some other perks while you write text.

When no errors were thrown, the editor still seemed to “act up”, and in some instances it even refused to work at all. Users were complaining that shortcuts randomly stopped working, the autocomplete popup didn’t disappear, and a host of other seemingly unrelated issues.

None of the odd behavior could be related back to logical errors in my code, since I did not touch the editor itself. Also, some errors were thrown deep inside the CodeMirror instance, which pointed towards something completely different causing this. I suspected pretty soon that the new Proxies had something to do with this.

So I took the code editor out of the data object of the editor component and just manually initialize it in the mount function. This made sure that Vue didn’t proxy the editor instance. That already fixed a host of issues we were experiencing.

What I forgot was that I also stored the open documents (which are instances of CodeMirror.Doc()) inside the component data. So I pulled the list of open documents out of the data object as well, and this fixed another set of issues. Finally, I made sure to de-proxy every piece of data that I transferred from the editor component to the code editor instance, as I found out that sometimes there were some rogue proxies hidden deeply inside the instance.

The morale of the story is that I did not only have to refactor the components using the migration guide by the Vue team, but that I also had to check all my components to see if there was some data which shouldn’t be there.

In the end, there are two additional conditions that need to be fulfilled when writing code using Vue 3 which didn’t apply in Vue 2: First, you should never store any complex objects, instances, and the likes inside your data – only primitives, simple objects, and arrays. No class objects, no functions, nothing like that. And second, whenever any data leaves your component – either by IPC or by passing it on to external code – you must always de-proxy it. The latter is even important when not dealing with Vue, which takes me to a last section for reflections.

Final Thoughts

Vue 3’s proxy-based reactivity is pretty fancy. It makes the Vue code much more performant, and makes it harder to miss updates inside the state. For instance, there is no more need to call Vue.set() to update certain properties, a nuisance caused by the limitations of the previous getter/setter system. However, this improvement comes with the drawback that proxies are much more aggressive.

Where previously we could get away with just sharing the data freely with the world, now we have to make sure to manually de-proxy any data before we attempt to share it with the outside world of the component.

This last point is even crucial to do if you don’t use Vue at all. As soon as you proxy an object in order to stay informed about every access to that object, the proxy will be bound to the local context you’re using it in.

What does this mean? Let us have a small thought experiment. Imagine you are working on one part of some website and you proxy a data object because then it becomes much easier to react to any access. And now imagine that you figure out that you could also use the data from this part of the website in a completely different place. If you now just naïvely send off that proxy instead of cloning the object, your own get- and set-handlers will always be called when that other part of your website accesses any property of that data object. This effectively leads to the ability of some remote part of your code to actually control the part that relies on the proxy handlers. This could lead to all sorts of weird behavior if you don’t keep track of which parts of your website have access to the proxy.

With these sorts of handy “magic” behavior JavaScript is very beginner friendly, because it hides away a lot of the complexity of modern code. This makes JavaScript a very good language to give newbies a non-frustrating intro to programming. But for professional or critical web apps, this can be disastrous. Proxies are just the latest example for this: Because you cannot “see” them since they behave like the object, you don’t know if you are actually dealing with a proxy, and there is no way of finding out. In other words: Any object in your code could silently, behind the curtain, call those almost “magic” functions that could tamper with your data. Proxy objects even have this in their specification: Those handlers are not for nothing called “traps”.

So make sure you always know what is happening, even if it is not obvious from the code you write.

Return to the post list