7. Errors and Application Design
7.1. Exception hierarchy
Every library exception derives from xmlio::XmlError:
ParseErrorMalformed XML, an unreadable file, or an invalid input source.
QueryErrorAn invalid XPath expression, a missing or non-unique required node, an unexpected node shape, or a structural container error.
ConversionErrorScalar text that cannot be converted completely to the requested C++ type. The exception retains both the path and offending text.
WriterErrorAn invalid tag-stack operation, malformed raw fragment, undeclared namespace prefix, or failed file output.
Catch the most specific type when recovery differs by failure category. Catch
XmlError at an executable or service boundary when all XML failures receive
the same user-facing treatment.
1/**
2 * @file error_handling.cpp
3 * @brief Handle schema versions and typed XML errors at an application boundary.
4 */
5
6#include <xmlio/xmlio.hpp>
7
8#include <iostream>
9#include <string>
10
11int main() {
12 auto input = xmlio::XMLReader::from_string(
13 "<settings><version>1</version><iterations>many</iterations></settings>");
14
15 try {
16 const int version = xmlio::read<int>(input, "/settings/version");
17 if (version != 1) {
18 throw xmlio::QueryError("/settings/version", "unsupported version");
19 }
20 static_cast<void>(xmlio::read<int>(input, "/settings/iterations"));
21 } catch (const xmlio::ConversionError& error) {
22 std::cout << "invalid value at " << error.path() << ": " << error.value()
23 << '\n';
24 } catch (const xmlio::XmlError& error) {
25 std::cout << "XML error: " << error.what() << '\n';
26 }
27}
7.2. Schema versions
Version handling belongs to the schema-owning application type. Read the version before interpreting fields whose meaning may have changed:
const int version = xmlio::read<int>(node, "version");
switch (version) {
case 1:
read_version_1(node, settings);
break;
case 2:
read_version_2(node, settings);
break;
default:
throw xmlio::QueryError("version", "unsupported settings version");
}
A converter should deserialize an old representation into an old C++ type, construct the new type explicitly, validate it, and write the current schema. This is clearer than scattering version checks across individual field reads.
7.3. Required fields and unknown fields
A required read fails when its path is absent or non-unique. Optional reads
make permitted absence explicit. xmlio does not reject unknown child tags;
that policy belongs to the schema owner. This allows forward-compatible readers
when new optional fields are added, while strict applications may perform an
additional schema-validation pass.
7.4. Application boundaries
Library-level read functions should throw rather than print or terminate.
The top-level application can then add the filename, operation, and user-facing
context once:
try {
auto input = xmlio::XMLReader::from_file(filename);
Settings settings;
read(input, "/settings", settings);
} catch (const xmlio::XmlError& error) {
std::cerr << "Cannot read " << filename << ": " << error.what() << '\n';
return 1;
}
This replaces the historical pattern of catching strings and terminating from deep inside a serializer.
7.5. Security and ownership
The libxml2 backend is private to xmlio. Parsing and fragment insertion use
non-network operation, so XML processing does not fetch remote resources.
Top-level readers own their parsed document; subreaders share that ownership and
remain valid after the original reader is moved or destroyed.
Readers and writers are movable but not copyable. This makes document ownership and output lifetimes explicit while still allowing return-by-value construction.