Error Handling
When a block cannot be extracted, the processor does not raise; it emits a BlockErrorEvent and keeps processing the stream. This guide covers the event's structure, the error codes, and the size limits that can trigger rejection.
BlockErrorEvent
| Field | Type | Description |
|---|---|---|
block_id |
str \| None |
Block identifier, when known |
reason |
str |
Human-readable failure description |
error_code |
BlockErrorCode \| None |
Machine-readable category (table below) |
syntax |
str |
Syntax that was accumulating the block |
start_line / end_line |
int / int \| None |
Where the failed block started and ended |
exception |
Exception \| None |
The original exception object, when one was raised |
Error codes
BlockErrorCode categorizes every failure:
| Code | Meaning |
|---|---|
VALIDATION_FAILED |
Block structure is valid, but a syntax or registry validator rejected it |
SIZE_EXCEEDED |
Block grew past max_block_size; rejected to prevent memory exhaustion |
UNCLOSED_BLOCK |
Block opened but never closed before the stream ended |
UNKNOWN_TYPE |
The syntax extracted a block_type that is not registered |
PARSE_FAILED |
Parsing failed: malformed YAML, invalid structure, or a parse() exception |
MISSING_METADATA |
Parse succeeded but returned no metadata |
MISSING_CONTENT |
Parse succeeded but returned no content |
SYNTAX_ERROR |
Syntax-specific custom validation failure |
Branch on the code for recovery logic:
from hother.streamblocks import BlockErrorCode, BlockErrorEvent
async for event in processor.process_stream(stream):
if isinstance(event, BlockErrorEvent):
if event.error_code == BlockErrorCode.SIZE_EXCEEDED:
logger.warning("block too large: %s", event.reason)
elif event.error_code == BlockErrorCode.UNCLOSED_BLOCK:
logger.info("incomplete block at stream end: %s", event.reason)
else:
logger.error("block rejected: %s", event.reason)
Inspecting the original exception
event.exception preserves the underlying error object, so you can handle YAML scanner errors, Pydantic validation errors, and type errors differently:
# Setup registry with basic syntax - no custom block types needed
registry = Registry(
syntax=Syntax.DELIMITER_FRONTMATTER,
)
# Create processor
processor = StreamBlockProcessor(registry)
async for event in processor.process_stream(simulated_stream(test_stream)):
if isinstance(event, BlockEndEvent):
block = event.get_block()
if block is not None:
extracted_blocks.append(block)
print("\nEXTRACTED Block:")
print(block.model_dump_json(indent=2))
elif isinstance(event, BlockErrorEvent):
rejected_blocks.append(event)
print(f"\nREJECTED: Block at lines {event.start_line}-{event.end_line}")
print(f" Syntax: {event.syntax}")
print(f" Reason: {event.reason}")
# Access structured exception information
if event.exception:
print(f" Exception Type: {type(event.exception).__name__}")
# Handle different exception types differently
if isinstance(event.exception, yaml.YAMLError):
print(" → YAML parsing error detected")
# Use getattr for attributes not in type stubs
problem = getattr(event.exception, "problem", "Unknown YAML error")
print(f" → Problem: {problem}")
problem_mark = getattr(event.exception, "problem_mark", None)
if problem_mark is not None:
line = getattr(problem_mark, "line", -1)
column = getattr(problem_mark, "column", -1)
print(f" → Location: line {line + 1}, column {column + 1}")
elif isinstance(event.exception, ValidationError):
print(" → Pydantic validation error detected")
print(event.exception)
print(" → Missing/invalid fields:")
for error in event.exception.errors():
field = ".".join(str(loc) for loc in error["loc"])
msg = error["msg"]
print(f" • {field}: {msg}")
elif isinstance(event.exception, TypeError):
print(" → Type error detected")
print(f" → Details: {event.exception}")
else:
print(f" → Other error: {event.exception}")
# Show block_id if available
if event.block_id:
print(f" Block ID: {event.block_id}")
For Pydantic ValidationError, event.exception.errors() lists exactly which metadata fields are missing or invalid. The full runnable script is in Basics examples.
Unclosed blocks at stream end
If a stream finishes while a block is still accumulating (the LLM was cut off, or never emitted the closing marker), the pending candidate is flushed as a BlockErrorEvent with code UNCLOSED_BLOCK and reason "Stream ended without closing marker". The accumulated lines are discarded rather than re-emitted as text, so treat this code as "partial block": start_line and end_line tell you which part of the stream was consumed by it.
Size limits
Two ProcessorConfig limits protect against runaway blocks:
| Option | Default | Behavior when exceeded |
|---|---|---|
max_block_size |
1_048_576 (1 MiB) |
Block rejected with SIZE_EXCEEDED |
max_line_length |
16_384 (16 KiB) |
Line truncated to the limit |
from hother.streamblocks.core.processor import ProcessorConfig
config = ProcessorConfig(max_block_size=2_097_152) # 2 MiB
processor = StreamBlockProcessor(registry, config=config)
Next steps
- Validation: reject bad blocks early, before content accumulates.
- Events: where
BlockErrorEventfits in the event stream. - Performance Tuning: the remaining
ProcessorConfigoptions.