loading...
Cover image for JS tip: Create debug friendly unique references using String
Zenika

JS tip: Create debug friendly unique references using String

nlepage profile image Nicolas Lepage ใƒป2 min read

Recently I've been facing a case where I needed to create some unique references.
I was creating a JS API and needed to maintain some internal state relative to elements created by the API.

In practice, I had a WeakMap where I maintained a private state for each public reference returned by the API.

Why a WeakMap?

The keys of a WeakMap are weak references, which means as soon as no one else holds the reference of a key, the key and its value get removed from the map and garbage collected.

The type of the public references didn't matter to me, I just needed unique references.
But it would be better if those references were easy to debug, which meant having at least a relevant toString() method.

โ›” Non choices โ›”

{}

What's the easiest way to create a unique reference in JS? Initializing an empty object!
But that doesn't satistfy the "easy to debug" requirement:

const ref = {}
console.log(`debug: ${ref}`)

// Output:
// debug: [object Object]

String

A string is easy to debug, you just print it:

let nextId = 1

const ref = `ref #${nextId++}`
console.log(`debug: ${ref}`)

// Output:
// debug: ref #1

But a string isn't a unique reference, anyone may obtain the same reference very easily.
And as a matter of fact, a WeakMap doesn't accept strings as keys, neither any other primitive type!

Symbol

The purpose of Symbol is to create unique references!
There I thought I had my solution...

let nextId = 1

const ref = Symbol(`ref #${nextId++}`)
console.log(`debug: ${ref}`)

// Output:
// TypeError: Cannot convert a Symbol value to a string

Wow! Far from what I expected...
Even though Symbol has a toString() method, putting it in a string interpolation yells at me!
Furthermore symbols are primitive values, hence WeakMap won't accept these as keys!

๐ŸŸข Solutions ๐ŸŸข

class

There I found myself forced to write my own class:

let nextId = 1
class Ref {
  constructor() { this.id = nextId++ }
  toString() { return `ref #${this.id}` }
}

const ref = new Ref()
console.log(`debug: ${ref}`)

// Output:
// debug: ref #1

This is exactly what I wanted, but it feels like I'm squashing a fly with a sledgehammer...
Do I really have to create my own type?

new String()

Yes it's possible to use the String constructor, and it is equivalent to using a class:

let nextId = 1

const ref = new String(`ref #${nextId++}`)
console.log(`debug: ${ref}`)

// Output:
// debug: ref #1

It does create a unique reference, which isn't of a primitive type.
Hence it may be used as a key for a WeakMap!

Your turn!

Do you have any other ideas? Share them with me!

Thanks for reading, give a โค๏ธ, leave a comment ๐Ÿ’ฌ, and follow me to get notified of my next posts.

Posted on by:

nlepage profile

Nicolas Lepage

@nlepage

Fullstack JS Dev by day, Gopher by night

Zenika

We are a software development company whose mission is to drive change via IT innovation. Many of our consultants have written books, do open-source contributions, teach classes and speak at popular meet-ups and conferences.

Discussion

markdown guide
 

I'd use Symbol as it's intended exactly for your use case.

Regarding your problems with Symbols:

1. Not used in strings

Why not just write

console.log('debug: %o', ref);

But I stopped constructing message strings for console.log(), I now just add parameters I want to log:

console.log('debug:', ref);

// With ES6 I use this improved variant:
console.log('debug', {ref});
console.log('debug', {ref, someVariable, anotherObject });

2. Symbols can't be used as keys in WeapMap

I just makes no sense to have primitive values as keys in a WeakMap, because they cannot be weakly referenced. That's why its forbidden.

Are you sure that you need a WeakMap in our situation? With a Map, Symbols can be used.

 

Thank you for your response.

  1. I agree with you, not being able to put a Symbol in a string interpolation isn't actually a problem.

  2. I'm aware of why primitive values may not be used in a WeakMap, even if this might have appeared a little unclear in my post ๐Ÿ˜…

Yes I'm sure I need a WeakMap:

The case is an API which returns unique references, and for each of these references, some internal state is maintained by the API.

As soon as the user of the API discards a unique reference, the internal state may be garbage collected from the WeakMap.

This avoids asking from the user of the API to "release" each reference once not used anymore.

 

Ok, I agree with you that a WeapMap is fine here, and therefore it should IMHO be possible to have Symbols as WeakMap keys.

I just found an interesting thread at github.com/tc39/ecma262/issues/1194 (TL;DR)

Unfortunately, because of the existence of non-unique Symbols (Symbol.for()) this won't come to ECMAScript :-/

So, the best options - as you mentioned - are new String() or object/class.

The latter with a ref property for better "debugability" and/or a toString() implementation (just an idea: debug info like the ref number could be held in a WeakMap if you want to hide it from the object, toString() could read from there)

Indeed interesting thread!

Too bad, I'd have prefered Symbols to be allowed as WeakMap keys...

Yes, primarily I've been using a WeakMap to hold sensitive data (which shouldn't be altered by any other way than calling the API), but it could also hold debug info.