DEV Community

Raphael Schweikert
Raphael Schweikert

Posted on

The case of the non-stringifyable value

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 throwing2.

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:

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

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:

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

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:

ToString in ECMAScript 2024 Language Specification

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

The specification of String(value) clarified things:

String(value)in ECMAScript 2024 Language Specification

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. 

Top comments (0)