7. Errors and Application Design

7.1. Exception hierarchy

Every library exception derives from xmlio::XmlError:

ParseError

Malformed XML, an unreadable file, or an invalid input source.

QueryError

An invalid XPath expression, a missing or non-unique required node, an unexpected node shape, or a structural container error.

ConversionError

Scalar text that cannot be converted completely to the requested C++ type. The exception retains both the path and offending text.

WriterError

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