Skip to main content

Error handling

The core of the C++ implementation is exception-free: every operation that can fail returns osf::Result<T> — a tl::expected<T, osf::Error>. Callers who prefer exceptions put the thin opt-in osf::throwing layer on top. The two styles can be mixed.

osf::Error and osf::Result<T>

struct Error {
Code code; // stable category — branch on this
std::string message; // human-readable detail — for display only
};

template <typename T>
using Result = tl::expected<T, Error>;

Basic idiom:

auto r = osf::DataManager::loadFromFile(path);
if (!r) {
log("load failed [{}]: {}",
osf::errorCategoryName(r.error().code), // stable name, e.g. "io_error"
r.error().message);
return;
}
osf::DataManager const& mgr = *r; // or r.value()

Rules:

  • Branch on code, display message only. The wording of the message is not part of the API and may change.
  • All Result returns are [[nodiscard]] — the compiler flags ignored errors.
  • errorCategoryName(code) returns a stable string identifier for logs (static, no ownership).
  • Result<void> signals pure success/failure operations (writer.start(), writeToFile(...), …): if (!r) ….

Error-code catalogue

Input and API errors

CodeMeaningTypical source
InvalidArgumentAPI precondition violated: invalid ChannelDef, unknown channel index at the writer, count == 0, writer in the wrong lifecycle phase, non-positive sample rateWriter
IoErrorfile/stream error: cannot open, read/write error, fsync failureeverywhere
NotFoundreserved for lookup APIs (channel lookups return nullptr instead)
Unknownfallback without a more specific category; code of a default-constructed Error
ParseErrorgeneric parse error when no more specific category fits (rare)parsers

Magic header

CodeMeaning
InvalidMagicHeaderthe first line is not a well-formed OSF header — usually "not an OSF file"
UnsupportedVersionheader parseable, but the identifier is none of the four accepted spellings (OSF4, OSF5, OCEAN_STREAM_FORMAT4, OCEAN_STREAMING_FORMAT4)
MagicHeaderTooLongno line break within 128 bytes — definitely not an OSF file

Metablock

CodeMeaning
InvalidMetablockstructural error: required field missing, number not parseable, sizeOfLengthValue ≠ 2/4 (would otherwise silently corrupt every block read), wrong root element
JsonParseErrorOSF5: the metablock body is not valid JSON (parser diagnostic in message)
XmlParseErrorOSF4: the metablock body is not well-formed XML (diagnostic + byte offset in message)
RemovedInSpecthe file uses a data type removed by spec rev 2026-05-04 (pair, triple, candata, gpsdata). Rejected hard — the old payload layout cannot be reproduced from a current build; the message names the replacement

Block stream

CodeMeaning
UnknownChannelIndexa block references a channel index without a metablock definition. Without the definition the width of the length field is unknown → corruption signal, hard abort
InvalidBlockpayload structurally broken (wrong length for the data type, equidistant block on a string channel, sample exceeds the streaming writer's block capacity, …)
ChannelMixedBlockTypesa channel delivers both equidistant (bcStartData/bcContinuedData) and timestamped blocks — forbidden by the spec
ContinuedDataWithoutStartbcContinuedData without a preceding bcStartData — without an open segment the continuation has no time base
RelStampWithoutAnchorbcContinuedRelStampData without a prior absolute timestamp — the deltas have no anchor
DataTypeMismatchrequested type ≠ stored type (e.g. asDoublesFlat on an int32 channel, asStrings on a binary channel)

What is deliberately not an error

SituationBehaviour
File ends mid-block (power loss)best-effort: all complete blocks are delivered, stats.blocksTruncated = 1, iteration ends cleanly
Unknown future data type/channel typethe channel parses as Unsupported, blocks are consumed aligned as Skipped, the remaining channels load normally
Deprecated/reserved control bytes (old field files)BlockKind::Skipped with a SkipReason, counters in stats
Deprecated channel fields (scale, offset, physicalunit1..3, …)silently tolerated and ignored (real field files all carry them)
Channel lookup with no hitnullptr, no Result

The throwing layer — osf::throwing

Header-only, opt-in (#include <osf/throwing.h>), deliberately not in the umbrella header <osf/osf.h> and not compiled into the library. Anyone who never includes it pulls in no exception machinery.

#include <osf/throwing.h>

try {
auto mgr = osf::throwing::load("measurement.osf"); // DataManager or throws
osf::throwing::writeToFile(mgr, "copy.osf");

osf::StreamingWriter w{path};
auto ch = osf::throwing::unwrap(w.addChannel(def)); // Result<T> -> T or throws
osf::throwing::unwrap(w.start());
osf::throwing::unwrap(w.writeTimestampedSample<double>(ch, ts, value));
osf::throwing::unwrap(w.close());
} catch (osf::Exception const& e) {
// e.what() — message (or category name if message empty)
// e.code() — Error::Code for programmatic branching
// e.error() — the complete structured osf::Error
}

The layer consists of exactly three building blocks:

Building blockPurpose
osf::Exception : std::runtime_errorcarries the complete osf::Error; lives in osf, not osf::throwing
osf::throwing::unwrap(Result<T>)universal adapter: extract the value or throw. Works with any Result of the core, including writer methods — so it needs no per-method throwing wrappers
osf::throwing::load / writeToFile / writeTothrowing counterparts of the most common high-level operations

Choosing a style in practice

  • Library/embedded code, hot paths, codebases with -fno-exceptions: stay on the Result core.
  • Application code with an existing exception strategy: use throwing at the outer edge; everything internal stays Result.
  • Mixed: unwrap selectively where an error would only propagate anyway — e.g. in a CLI main that has a top-level try/catch.

Special case: writer sticky errors

The StreamingWriter remembers the first I/O error ("Broken" state) and returns it on every subsequent call, including close(). In write loops a single error check per iteration is therefore enough; the cause is not lost even when evaluated only at the end.