The case of the non-stringifyable value
Tags: javascript
Published: 2023-09-05 15:24:40

When I see someone calling `someVar.toString()`, I usually advise them to use `String(someVar)`[^1] instead, as it handles `null` and `undefined` cases gracefully (depending on the definition of gracefully, of course) instead of throwing[^2].

For the longest time I was under the impression that calling `String(someVar)` would never throw, for any value of `someVar`. However, I was wrong. There are two cases where it also throws:

* When passed an object whose `Symbol.toPrimitive` exists but isn’t callable or calling it with `'string'` as first argument returns a non-primitive or throws.
* When passed an object that doesn’t have a `Symbol.toPrimitive` property and `toString` or `valueOf` each either don’t exist, aren’t callable, return a non-primitive, or throw.

Most likely you’ll encounter the case where none of these methods are declared when handling an object that doesn’t have a prototype, e.g. if it was created using `Object.create(null)`. In that case, my advice wouldn’t have been better or worse because both approaches throw.

But we can also craft an object specifically to throw when passed to `String` but not when calling `toString` on it:

{% runkit %}
const o = {
  \[Symbol.toPrimitive\]() {
    return {};
  },
};
console.log(o.toString());
console.log(String(o));
{% endrunkit %}

In this case, my initial advice would be completely wrong and `toString` would have been the better option.

Of course, we could also easily have crafted the opposite:

{% runkit %}
const o = {
  \[Symbol.toPrimitive\]() {
    return "cool string";
  },
  toString: 42,
};
console.log(String(o));
console.log(o.toString());
{% endrunkit %}

I stand by the stance that `String(someVar)` is usually the better approach in real-world scenarios (it also looks nicer) but I’ve come around to thinking that being explicit about `null` and `undefined` is better than being implicit so I always advice for having null checks anyway.

Investigating this also helped me clear up another misconception. The `String` construction (when used as a function) doesn’t just apply the abstract operation `ToString` as defined by ECMAScript.

`ToString` is specified [as follows](https://tc39.es/ecma262/#sec-tostring):

![ToString in ECMAScript 2024 Language Specification](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/liw6cksd408z8icfinqy.png)

But I already knew that `String(Symbol('a'))` doesn’t throw, so something else must be going on there.

[The specification of `String(value)`](https://tc39.es/ecma262/#sec-string-constructor-string-value) clarified things:

![String(value)in ECMAScript 2024 Language Specification](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wa90w5vzunxc57xv7kfj.png)

That’s it, symbols are special-cased in `String(value)`, they won’t ever throw!

But symbols will throw when used in an untagged template string literal as there, the `ToString` operation is used.

[^1]: Or `${someVar}` in a template string literal, which amounts to almost the same thing.
[^2]: Technically, it’s not `toString` that throws, it’s the property lookup on `someVar` that throws.