I believe it's worth copying here the reasoning behind that "peculiar error handling" note in the io::load() API documentation, for future reference.
"peculiar" is a nice way of saying that the behavior is quite weird and IMHO makes little sense for an API. [...]
The reason why it's weird is that it mixes both error handling styles: exception-based and exception-free. It returns nullptr without throwing in some error cases but throws an exception in other cases. A caller that really cares about proper error handling would have to both check for nullptr and use try/catch.
Moreover, returning nullptr provides no diagnostic information whatsoever on the nature of the error, so debugging is quite literally impossible (and I speak from experience). In the rare case where calling code really does not care about the error (e.g. you want a silent failure because there's a fallback), it's better to just wrap the call in a try/catch and ignore any exceptions.
And you can get further proof that this API is far from ideal by looking at how it's used in other projects. In many cases, the error handling is either wrong (ndns, nlsr) or, in the case of ndn-nac, the code checks for nullptr and then (ironically) throws an exception... and if you're going to use exceptions anyway, then you're better served by letting load() propagate whatever exception is thrown, which at least tells you the error reason and can be debugged.
one more reason: load() and save() are asymmetric, because the latter always throws on error, as opposed to e.g., return a bool which would be the corresponding exception-free style. So, this API is just very messy and hard to reason about.
tl;dr: I strongly believe we should discourage using this load function. I can be convinced to go a step further and formally deprecate it or even outright replace it with the new loadTlv.