Skip to content

3. Failure handling and API design

List each operation’s failure classes before choosing an API. Preserve errors that callers can act on, attach deterministic context at one layer, and convert to a process exit status only at the entry point. Exercise allocation pressure, truncated input, repeated calls, partial writes, timeouts, and cleanup after an error. Tests should assert state as well as the returned error.

Benchmark the operation users face: include setup only when users pay it, consume results, warm up comparable runtimes, and report distributions rather than a single best sample. Record versions, target, optimization mode, input, and command so the result can become a future baseline.

Keep modules cohesive and exports few. Separate wire or disk formats from in-memory models, make invalid states hard to construct, and prefer one explicit conversion over implicit policy spread across call sites.

Tested example

Map typed domain failures to stable CLI messages and exit codes in one place.

Excerpt in focus

curriculum/main-evidence/projects/ledger/src/main.fl:20–55

curriculum/main-evidence/projects/ledger/src/main.fl (excerpt)
command := values[1].view();
error result = none;
if command == "create" and values.len() == 3 {
result = commands::create(values[2].view());
} else if command == "append" and values.len() == 4 {
result = commands::append(values[2].view(), values[3].view());
} else if command == "verify" and values.len() == 3 {
result = commands::verify(values[2].view());
} else if command == "list" and values.len() == 3 {
result = commands::list(values[2].view());
} else if command == "help" or command == "--help" or command == "-h" {
print_usage();
return 0;
} else {
result = model::LedgerError.Usage;
}
if result == none {
return 0;
}
if result == model::LedgerError.Usage {
print_usage();
return 2;
}
if result == model::LedgerError.InvalidFormat or
result == model::LedgerError.PayloadTooLarge {
fmt::eprintln("ledger: invalid ledger format");
return 4;
}
if result == model::LedgerError.IntegrityFailure {
fmt::eprintln("ledger: integrity verification failed");
return 5;
}
fmt::eprintln("ledger: operation failed");
return 3;

This excerpt depends on the other package files. The complete compiled source is available below.

Complete package source (8 files)

longbow.toml

longbow.toml
[package]
name = "ledger"
version = "0.1.0"
kind = "program"
[dependencies]

longbow.lock

longbow.lock
{
"schema": 1,
"root": "root",
"packages": [
{
"id": "root",
"identity": "local/ledger",
"source": {
"kind": "workspace",
"path": "."
},
"version": "0.1.0",
"dependencies": {
"std": "toolchain-std"
}
},
{
"id": "toolchain-std",
"identity": "flecha/std",
"source": {
"kind": "toolchain"
},
"dependencies": {}
}
]
}

src/codec.fl

src/codec.fl
module codec;
@imports
:: root::model
:: lib::std::bytes::buffer
:: lib::std::bytes::sequence
:: lib::std::containers::collections
:: lib::std::cryptography::sha2
:: lib::std::encoding::binary
:: lib::std::text::strings;
export const stringv FILE_HEADER = "FLECHA-LEDGER\n1\n";
const stringv HASH_DOMAIN = "FLECHA-LEDGER-BLOCK-V1\n";
const usize RECORD_HEADER_SIZE = 48;
const usize DIGEST_SIZE = 32;
const usize RECORD_OVERHEAD = RECORD_HEADER_SIZE + DIGEST_SIZE;
export (buffer::Buffer, [32]u8, error) encode_block(
u64 index,
[]const u8 previousHash,
stringv payload
) {
usize payloadLength = payload.len().cast<usize>;
usize capacity = RECORD_OVERHEAD + payloadLength;
if capacity < payloadLength or previousHash.len() != DIGEST_SIZE {
empty, allocationError := buffer::Buffer::withCapacity(0);
if allocationError != none {
return (empty, none, allocationError);
}
return (empty, none, model::LedgerError.PayloadTooLarge);
}
output, allocationError := buffer::Buffer::withCapacity(capacity);
if allocationError != none {
return (output, none, allocationError);
}
[48]u8 header = none;
headerWriter := binary::Writer::init(header[:]);
error writeError = headerWriter.writeBig<u64>(index);
if writeError == none {
writeError = headerWriter.writeBig<u64>(payloadLength.cast<u64>);
}
if writeError == none {
writeError = headerWriter.write(previousHash);
}
if writeError != none {
return (output, none, writeError);
}
digest, digestError := hash_block(header[:], payload);
if digestError != none {
return (output, none, digestError);
}
[32]u8 digestBytes = model::copy_digest(digest.bytes());
writeError = output.write(header[:]);
if writeError == none {
writeError = output.write(payload.cast<[]const u8>);
}
if writeError == none {
writeError = output.write(digestBytes[:]);
}
return (output, digestBytes, writeError);
}
(sha2::Sha256Digest, error) hash_block(
[]const u8 header,
stringv payload
) {
hasher := sha2::Sha256::init();
error digestError = hasher.update(HASH_DOMAIN.cast<[]const u8>);
if digestError == none {
digestError = hasher.update(header);
}
if digestError == none {
digestError = hasher.update(payload.cast<[]const u8>);
}
if digestError != none {
return (none, digestError);
}
return hasher.finalize();
}
export (collections::Array<model::Block>, error) parse(stringv contents) {
blocks := collections::Array<model::Block>::init();
if contents.len() < FILE_HEADER.len() or
contents.substr(0, FILE_HEADER.len()) != FILE_HEADER {
return (blocks, model::LedgerError.InvalidFormat);
}
source := contents.cast<[]const u8>;
reader := binary::Reader::init(source[FILE_HEADER.len().cast<usize>:]);
u64 expectedIndex = 0;
[32]u8 expectedPrevious = none;
while !reader.empty() {
if reader.remaining() < RECORD_OVERHEAD {
return (blocks, model::LedgerError.InvalidFormat);
}
index, indexError := reader.readBig<u64>();
payloadLength, lengthError := reader.readBig<u64>();
if indexError != none or lengthError != none {
return (blocks, model::LedgerError.InvalidFormat);
}
[32]u8 previousValue = none;
{
previous, previousError := reader.read(DIGEST_SIZE);
if previousError != none {
return (blocks, model::LedgerError.InvalidFormat);
}
previousValue = model::copy_digest(previous);
}
if payloadLength > (reader.remaining() - DIGEST_SIZE).cast<u64> {
return (blocks, model::LedgerError.InvalidFormat);
}
string payload = none;
{
payloadBytes, payloadError := reader.read(
payloadLength.cast<usize>
);
if payloadError != none {
return (blocks, model::LedgerError.InvalidFormat);
}
error allocationError;
payload, allocationError = strings::string_from_bytes(payloadBytes);
if allocationError != none {
return (blocks, allocationError);
}
}
[32]u8 digestValue = none;
{
storedDigest, storedError := reader.read(DIGEST_SIZE);
if storedError != none {
return (blocks, model::LedgerError.InvalidFormat);
}
digestValue = model::copy_digest(storedDigest);
}
if index != expectedIndex or
!sequence::constant_time_equal(
previousValue[:],
expectedPrevious[:]
) {
return (blocks, model::LedgerError.IntegrityFailure);
}
[48]u8 canonicalHeader = none;
canonicalWriter := binary::Writer::init(canonicalHeader[:]);
error canonicalError = canonicalWriter.writeBig<u64>(index);
if canonicalError == none {
canonicalError = canonicalWriter.writeBig<u64>(payloadLength);
}
if canonicalError == none {
canonicalError = canonicalWriter.write(previousValue[:]);
}
if canonicalError != none {
return (blocks, canonicalError);
}
calculated, digestError := hash_block(
canonicalHeader[:],
payload.view()
);
if digestError != none {
return (blocks, digestError);
}
if !sequence::constant_time_equal(calculated.bytes(), digestValue[:]) {
return (blocks, model::LedgerError.IntegrityFailure);
}
pushError := blocks.push(model::Block::init(
index,
previousValue,
payload,
digestValue
));
if pushError != none {
return (blocks, pushError);
}
expectedPrevious = digestValue;
expectedIndex++;
if expectedIndex == 0 and !reader.empty() {
return (blocks, model::LedgerError.InvalidFormat);
}
}
return (blocks, none);
}

src/codec_test.fl

src/codec_test.fl
module codec_test;
@imports
:: root::codec
:: root::model
:: lib::std::bytes::buffer
:: lib::std::bytes::sequence
:: lib::std::encoding::hex
:: lib::std::tests::asserts
:: lib::std::tests::testing
:: lib::std::text::strings;
const stringv ALPHA_DIGEST =
"aba5ed27f8ee302525deccc60adfb950fd3d71699082bb3b5c0bcad6a36ec139";
const stringv BETA_DIGEST =
"d4298ba679357d6f92c5ea1da21f8895a3e6d514b35e51b3e68081744d9f6b4a";
const stringv ALPHA_RECORD =
"000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000000616c706861aba5ed27f8ee302525deccc60adfb950fd3d71699082bb3b5c0bcad6a36ec139";
test canonical_block_matches_published_golden_vector(&testing::UnitTest t) {
[32]u8 genesis = none;
record, digest, encodeError := codec::encode_block(
0,
genesis[:],
"alpha"
);
asserts::is_true(t, encodeError == none, "golden block encodes");
if encodeError != none {
return;
}
recordHex, recordHexError := hex::encode_bytes(record.bytes());
digestHex, digestHexError := hex::encode_bytes(digest[:]);
asserts::is_true(
t,
recordHexError == none and recordHex.view() == ALPHA_RECORD,
"canonical record bytes match the golden vector"
);
asserts::is_true(
t,
digestHexError == none and digestHex.view() == ALPHA_DIGEST,
"domain-separated block hash matches the golden vector"
);
}
test chain_round_trips_and_links_every_block(&testing::UnitTest t) {
chain, chainError := make_chain("alpha", "beta");
asserts::is_true(t, chainError == none, "chain construction succeeds");
if chainError != none {
return;
}
blocks, parseError := codec::parse(chain.view());
asserts::is_true(t, parseError == none, "canonical chain parses");
asserts::is_true(t, blocks.len() == 2, "both blocks reload");
if parseError != none or blocks.len() != 2 {
return;
}
values := blocks.view();
firstDigest, firstHexError := hex::encode_bytes(values[0].hash());
secondDigest, secondHexError := hex::encode_bytes(values[1].hash());
asserts::is_true(
t,
values[0].index() == 0 and values[0].payload() == "alpha",
"the genesis block preserves its fields"
);
asserts::is_true(
t,
values[1].index() == 1 and values[1].payload() == "beta",
"the second block preserves its fields"
);
asserts::is_true(
t,
firstHexError == none and firstDigest.view() == ALPHA_DIGEST,
"genesis digest is stable"
);
asserts::is_true(
t,
secondHexError == none and secondDigest.view() == BETA_DIGEST,
"head digest is stable"
);
asserts::is_true(
t,
sequence::equal(values[1].previousHash(), values[0].hash()),
"the second block links to the genesis digest"
);
}
test identical_chains_produce_identical_bytes(&testing::UnitTest t) {
first, firstError := make_chain("alpha", "beta");
second, secondError := make_chain("alpha", "beta");
asserts::is_true(
t,
firstError == none and secondError == none and
first.view() == second.view(),
"the canonical chain is reproducible"
);
}
test malformed_and_truncated_storage_is_rejected(&testing::UnitTest t) {
malformed, malformedError := codec::parse("not-a-ledger");
asserts::is_true(
t,
malformedError == model::LedgerError.InvalidFormat and
malformed.empty(),
"a malformed header is rejected"
);
chain, chainError := make_chain("alpha", "beta");
if chainError != none {
t->fail();
return;
}
truncated, truncatedError := codec::parse(
chain.view().substr(0, chain.len() - 1)
);
asserts::is_true(
t,
truncatedError == model::LedgerError.InvalidFormat and
truncated.len() == 1,
"a truncated final digest is rejected after the valid prefix"
);
}
test tampered_payload_and_digest_are_rejected(&testing::UnitTest t) {
chain, chainError := make_chain("alpha", "beta");
if chainError != none {
t->fail();
return;
}
tamperedPayload, payloadError := changed_byte(chain.view(), 64, 'A');
if payloadError != none {
t->fail();
return;
}
_, tamperError := codec::parse(tamperedPayload.view());
asserts::is_true(
t,
tamperError == model::LedgerError.IntegrityFailure,
"payload tampering is detected"
);
tamperedDigest, digestError := changed_byte(chain.view(), 84, 0);
if digestError != none {
t->fail();
return;
}
_, storedDigestError := codec::parse(tamperedDigest.view());
asserts::is_true(
t,
storedDigestError == model::LedgerError.IntegrityFailure,
"stored digest tampering is detected"
);
}
test reordered_blocks_are_rejected(&testing::UnitTest t) {
chain, chainError := make_chain("alpha", "bravo");
if chainError != none {
t->fail();
return;
}
source := chain.view().cast<[]const u8>;
reordered, allocationError := buffer::Buffer::withCapacity(source.len());
if allocationError != none or
reordered.write(codec::FILE_HEADER.cast<[]const u8>) != none or
reordered.write(source[101:186]) != none or
reordered.write(source[16:101]) != none {
t->fail();
return;
}
reorderedText, textError := strings::string_from_bytes(reordered.bytes());
if textError != none {
t->fail();
return;
}
_, parseError := codec::parse(reorderedText.view());
asserts::is_true(
t,
parseError == model::LedgerError.IntegrityFailure,
"record reordering breaks the index and hash chain"
);
}
(string, error) make_chain(stringv firstPayload, stringv secondPayload) {
output, allocationError := buffer::Buffer::withCapacity(256);
if allocationError != none {
return (none, allocationError);
}
error writeError = output.write(codec::FILE_HEADER.cast<[]const u8>);
if writeError != none {
return (none, writeError);
}
[32]u8 genesis = none;
first, firstDigest, firstError := codec::encode_block(
0,
genesis[:],
firstPayload
);
if firstError != none {
return (none, firstError);
}
second, _, secondError := codec::encode_block(
1,
firstDigest[:],
secondPayload
);
if secondError != none {
return (none, secondError);
}
writeError = output.write(first.bytes());
if writeError == none {
writeError = output.write(second.bytes());
}
if writeError != none {
return (none, writeError);
}
return strings::string_from_bytes(output.bytes());
}
(string, error) changed_byte(stringv source, usize offset, u8 value) {
bytes, allocationError := new<own []u8>(source.len().cast<usize>);
if allocationError != none {
return (none, allocationError);
}
loop usize index = 0; index < bytes.len(); index++ {
bytes[index] = source[index.cast<u32>];
}
bytes[offset] = value;
return strings::string_from_bytes(bytes[:]);
}

src/commands.fl

src/commands.fl
module commands;
@imports
:: root::storage
:: lib::std::encoding::hex
:: lib::std::fmt;
export error create(stringv path) {
result := storage::create(path);
if result == none {
fmt::println("created: 0 blocks");
}
return result;
}
export error append(stringv path, stringv payload) {
index, digest, appendError := storage::append(path, payload);
if appendError != none {
return appendError;
}
fmt::print("appended: ");
fmt::print(index);
fmt::print(" ");
fmt::println(digest.view());
return none;
}
export error verify(stringv path) {
blocks, loadError := storage::load(path);
if loadError != none {
return loadError;
}
fmt::print("verified: ");
fmt::print(blocks.len());
fmt::print(" blocks");
if !blocks.empty() {
values := blocks.view();
head, hexError := hex::encode_bytes(values[values.len() - 1].hash());
if hexError != none {
return hexError;
}
fmt::print(" ");
fmt::print(head.view());
}
fmt::println("");
return none;
}
export error list(stringv path) {
blocks, loadError := storage::load(path);
if loadError != none {
return loadError;
}
values := blocks.view();
loop usize index = 0; index < values.len(); index++ {
digest, digestError := hex::encode_bytes(values[index].hash());
if digestError != none {
return digestError;
}
payload, payloadError := hex::encode(values[index].payload());
if payloadError != none {
return payloadError;
}
fmt::print(values[index].index());
fmt::print(" ");
fmt::print(digest.view());
fmt::print(" ");
fmt::println(payload.view());
}
return none;
}

src/main.fl

src/main.fl
module main;
@imports
:: root::commands
:: root::model
:: lib::std::fmt
:: lib::std::os::arguments;
i32 main() {
values, captureError := arguments::capture();
if captureError != none {
fmt::eprintln("ledger: cannot capture command-line arguments");
return 3;
}
if values.len() < 2 {
print_usage();
return 2;
}
command := values[1].view();
error result = none;
if command == "create" and values.len() == 3 {
result = commands::create(values[2].view());
} else if command == "append" and values.len() == 4 {
result = commands::append(values[2].view(), values[3].view());
} else if command == "verify" and values.len() == 3 {
result = commands::verify(values[2].view());
} else if command == "list" and values.len() == 3 {
result = commands::list(values[2].view());
} else if command == "help" or command == "--help" or command == "-h" {
print_usage();
return 0;
} else {
result = model::LedgerError.Usage;
}
if result == none {
return 0;
}
if result == model::LedgerError.Usage {
print_usage();
return 2;
}
if result == model::LedgerError.InvalidFormat or
result == model::LedgerError.PayloadTooLarge {
fmt::eprintln("ledger: invalid ledger format");
return 4;
}
if result == model::LedgerError.IntegrityFailure {
fmt::eprintln("ledger: integrity verification failed");
return 5;
}
fmt::eprintln("ledger: operation failed");
return 3;
}
void print_usage() {
fmt::println("ledger - deterministic append-only SHA-256 chain");
fmt::println("");
fmt::println("usage:");
fmt::println(" ledger create <file>");
fmt::println(" ledger append <file> <payload>");
fmt::println(" ledger verify <file>");
fmt::println(" ledger list <file>");
}

src/model.fl

src/model.fl
module model;
export error LedgerError {
Usage,
InvalidFormat,
IntegrityFailure,
PayloadTooLarge
}
export structure Block {
private u64 blockIndex;
private [32]u8 previousDigest;
private string payloadValue;
private [32]u8 blockDigest;
};
Block Block::init(
u64 blockIndex,
[32]u8 previousDigest,
string payloadValue,
[32]u8 blockDigest
) override {
return build Block {
self.blockIndex = blockIndex;
self.previousDigest = previousDigest;
self.payloadValue = payloadValue;
self.blockDigest = blockDigest;
};
}
u64 Block::index(&const self) {
return self->blockIndex;
}
[]const u8 Block::previousHash(&const self) <-self {
return self->previousDigest[:];
}
stringv Block::payload(&const self) <-self {
return self->payloadValue.view();
}
[]const u8 Block::hash(&const self) <-self {
return self->blockDigest[:];
}
export [32]u8 copy_digest([]const u8 source) {
[32]u8 result = none;
loop usize index = 0; index < result.len(); index++ {
result[index] = source[index];
}
return result;
}

src/storage.fl

src/storage.fl
module storage;
@imports
:: root::codec
:: root::model
:: lib::std::containers::collections
:: lib::std::encoding::hex
:: lib::std::io::file
:: lib::std::io::filesystem;
export error create(stringv path) {
options := build file::OpenOptions {
self.write = true;
self.createNew = true;
};
output, openError := file::File::open(path, options);
if openError != none {
return openError;
}
error result = output.writeAll(codec::FILE_HEADER.cast<[]const u8>);
if result == none {
result = output.flush();
}
closeError := output.close();
if result == none {
result = closeError;
}
if result != none {
error _ = filesystem::remove_file(path);
}
return result;
}
export (collections::Array<model::Block>, error) load(stringv path) {
contents, readError := filesystem::read_file(path);
if readError != none {
return (collections::Array<model::Block>::init(), readError);
}
return codec::parse(contents.view());
}
export (u64, string, error) append(stringv path, stringv payload) {
blocks, loadError := load(path);
if loadError != none {
return (none, none, loadError);
}
u64 index = blocks.len().cast<u64>;
[32]u8 previous = none;
if !blocks.empty() {
values := blocks.view();
previous = model::copy_digest(values[values.len() - 1].hash());
}
record, digest, encodeError := codec::encode_block(
index,
previous[:],
payload
);
if encodeError != none {
return (none, none, encodeError);
}
output, openError := file::File::open(path, file::OpenOptions::appendOnly());
if openError != none {
return (none, none, openError);
}
error result = output.writeAll(record.bytes());
if result == none {
result = output.flush();
}
closeError := output.close();
if result == none {
result = closeError;
}
if result != none {
return (none, none, result);
}
digestText, hexError := hex::encode_bytes(digest[:]);
return (index, digestText, hexError);
}

The CLI boundary translates typed ledger errors once. Domain modules remain independent of terminal wording, while callers receive stable messages and exit codes suitable for automation.

Commands

Commands
longbow check curriculum/main-evidence/projects/ledger
longbow run curriculum/main-evidence/projects/ledger -- help

Expected output

Expected output
ledger - deterministic append-only SHA-256 chain
usage:
ledger create <file>
ledger append <file> <payload>
ledger verify <file>
ledger list <file>
exit status: 0

Related reference: /examples/ledger/src/main/.