Working with Diagnostic Data

Diagnostics turn exceptions into structured, user-facing information. This page explains how to inspect a diagnostic, build a complete document from an exception and its causes, and render that document for logs or an interactive terminal.

One Exception, One Diagnostic

Diagnostic describes one error. It can expose a source name, source path, and CodeLocation, and it converts its domain-specific details into a neutral TextDocument.

Call Exception::diagnostic() when you need the diagnostic for the current exception only. The base implementation creates a compact diagnostic from toString(); richer domain errors override it to preserve structured fields.

err/Diagnostic/InspectDiagnostic.cpp

/// `diagnostic()` returns the structured description for one exception.
/// Its source accessors let handlers inspect optional metadata without knowing the concrete exception type.
void inspectDiagnostic() {
    auto context = el::ApplicationErrorContext{"The score could not be read."_el};
    context.setSourceName("夜の演奏"_el)
        .setSourcePath("scores/夜の演奏.music"_el)
        .setCodeLocation(el::CodeLocation{el::LineIndex{3}, el::ColumnIndex{5}});
    const auto diagnostic = el::ApplicationError{std::move(context), {}}.diagnostic();
    const auto location = diagnostic->location();

    el::io::printLine("Source: "_el, diagnostic->sourceName());
    el::io::printLine("Path: "_el, diagnostic->sourcePath());
    el::io::printLine("Location: "_el, location.line().toSizeT() + 1, ":"_el, location.column().toSizeT() + 1);
}

Output

Source: 夜の演奏
Path: scores/夜の演奏.music
Location: 4:6

All source metadata is optional. Check the returned values before displaying them in custom interfaces, and remember that line and column indices are zero-based values even though the standard diagnostic renderers display them as one-based positions.

Build a Complete Diagnostic Document

Most reporting boundaries need more than one diagnostic. DiagnosticHelper normalizes a std::exception and provides two conversions:

err/Diagnostic/BuildDiagnosticDocument.cpp

/// `DiagnosticHelper` converts an exception into one diagnostic or a complete document containing every cause.
/// Use `toDocument()` at a reporting boundary so valuable information from translated failures is not lost.
void buildDiagnosticDocument() {
    try {
        try {
            throw std::runtime_error{"MIDI input stopped responding"};
        } catch (const std::runtime_error &) {
            throw el::RuntimeError{"The digital-piano input could not be read."_el, std::current_exception()};
        }
    } catch (const el::RuntimeError &error) {
        auto document = el::DiagnosticHelper{error}.toDocument();
        el::io::printLine(document.toString());
    }
}

Output

Error: The digital-piano input could not be read.
Caused By:
  MIDI input stopped responding

Use the static DiagnosticHelper::documentFromError() when you already have a std::exception_ptr instead of a caught reference. Foreign standard exceptions become escaped fallback diagnostics, and unknown exceptions receive a neutral message.

Render the Document for Its Destination

A diagnostic document contains semantic nodes rather than terminal control sequences or preformatted log text. Choose the renderer at the final destination:

When you use Application::run(), this reporting pipeline is already in place: Application builds the complete diagnostic document, including causes, and renders it through its system output. Render a document manually only when you need a custom reporting boundary, another destination, a preview, or code that does not use Application.

err/Diagnostic/RenderDiagnosticDocument.cpp

/// Diagnostic documents are renderer-neutral `TextDocument` trees.
/// The same document can become plain text for logs or styled terminal output for an interactive application.
void renderDiagnosticDocument() {
    auto context = el::ApplicationErrorContext{
        "The score could not be read."_el,
        "The tempo-marking value is out of range."_el,
    };
    context.setSourcePath("scores/朝の合奏.music"_el)
        .setCodeLocation(el::CodeLocation{el::LineIndex{6}, el::ColumnIndex{14}});
    const auto error = el::ApplicationError{std::move(context), {}};
    const auto document = el::DiagnosticHelper{error}.toDocument();

    el::io::printLine("--- Plain text ---"_el);
    el::io::printLine(document.toString());
    el::io::printLine("--- Terminal document ---"_el);
    const auto renderer = el::cterm::TerminalDocumentRenderer{el::application().systemOutputStyle()};
    renderer.renderTo(*el::application().terminal(), document);
}

Output

--- Plain text ---
Error: The score could not be read.
  The tempo-marking value is out of range.
Error Source:
  Path:   scores/朝の合奏.music
  Line:   7
  Column: 15
--- Terminal document ---

  The score could not be read.

  The tempo-marking value is out of range.

Error Source:
  Path:   scores/朝の合奏.music
  Line:   7
  Column: 15

Diagnostic::toString() is a convenient plain-text rendering of one diagnostic. It is not a substitute for DiagnosticHelper::toDocument() when an exception may have causes.

Display Text and Localization

Diagnostic conversion uses the application’s DisplayText registry for structural wording such as section labels and cause headings. Register a compiled catalog to customize this wording, or install a translator to translate the resolved texts. The compiled English Core catalog supplies the built-in defaults.

Domain-authored titles and descriptions are kept separate from this structural wording. Translate application text in the application, and use the registry to adapt reusable diagnostic labels. This keeps domain context intact while allowing the same semantic document to be rendered for different audiences.

Read Writing Custom Exceptions to create diagnostics for your own domain without coupling the exception itself to a particular renderer.

Complete demo sources

err/Diagnostic/InspectDiagnostic.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include <DemoCommon.hpp>
#include <erbsland/ApplicationError.hpp>
#include <erbsland/ApplicationErrorContext.hpp>
#include <erbsland/CodeLocation.hpp>
#include <erbsland/ColumnIndex.hpp>
#include <erbsland/LineIndex.hpp>
#include <erbsland/StandardStreams.hpp>

namespace demo {

/// `diagnostic()` returns the structured description for one exception.
/// Its source accessors let handlers inspect optional metadata without knowing the concrete exception type.
void inspectDiagnostic() {
    auto context = el::ApplicationErrorContext{"The score could not be read."_el};
    context.setSourceName("夜の演奏"_el)
        .setSourcePath("scores/夜の演奏.music"_el)
        .setCodeLocation(el::CodeLocation{el::LineIndex{3}, el::ColumnIndex{5}});
    const auto diagnostic = el::ApplicationError{std::move(context), {}}.diagnostic();
    const auto location = diagnostic->location();

    el::io::printLine("Source: "_el, diagnostic->sourceName());
    el::io::printLine("Path: "_el, diagnostic->sourcePath());
    el::io::printLine("Location: "_el, location.line().toSizeT() + 1, ":"_el, location.column().toSizeT() + 1);
}

}

err/Diagnostic/BuildDiagnosticDocument.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include <DemoCommon.hpp>
#include <erbsland/DiagnosticHelper.hpp>
#include <erbsland/RuntimeError.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/TextDocument.hpp>

#include <stdexcept>

namespace demo {

/// `DiagnosticHelper` converts an exception into one diagnostic or a complete document containing every cause.
/// Use `toDocument()` at a reporting boundary so valuable information from translated failures is not lost.
void buildDiagnosticDocument() {
    try {
        try {
            throw std::runtime_error{"MIDI input stopped responding"};
        } catch (const std::runtime_error &) {
            throw el::RuntimeError{"The digital-piano input could not be read."_el, std::current_exception()};
        }
    } catch (const el::RuntimeError &error) {
        auto document = el::DiagnosticHelper{error}.toDocument();
        el::io::printLine(document.toString());
    }
}

}

err/Diagnostic/RenderDiagnosticDocument.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include <DemoCommon.hpp>
#include <erbsland/all_cterm.hpp>
#include <erbsland/ApplicationError.hpp>
#include <erbsland/ApplicationErrorContext.hpp>
#include <erbsland/CodeLocation.hpp>
#include <erbsland/ColumnIndex.hpp>
#include <erbsland/cterm/TerminalDocumentRenderer.hpp>
#include <erbsland/DiagnosticHelper.hpp>
#include <erbsland/LineIndex.hpp>
#include <erbsland/StandardStreams.hpp>

namespace demo {

/// Diagnostic documents are renderer-neutral `TextDocument` trees.
/// The same document can become plain text for logs or styled terminal output for an interactive application.
void renderDiagnosticDocument() {
    auto context = el::ApplicationErrorContext{
        "The score could not be read."_el,
        "The tempo-marking value is out of range."_el,
    };
    context.setSourcePath("scores/朝の合奏.music"_el)
        .setCodeLocation(el::CodeLocation{el::LineIndex{6}, el::ColumnIndex{14}});
    const auto error = el::ApplicationError{std::move(context), {}};
    const auto document = el::DiagnosticHelper{error}.toDocument();

    el::io::printLine("--- Plain text ---"_el);
    el::io::printLine(document.toString());
    el::io::printLine("--- Terminal document ---"_el);
    const auto renderer = el::cterm::TerminalDocumentRenderer{el::application().systemOutputStyle()};
    renderer.renderTo(*el::application().terminal(), document);
}

}