Error Handling

Viper uses a single exception type for all errors: ViperError. This chapter explains how to catch, interpret, and recover from errors in your Python code.

The ViperError Exception

All Viper operations that fail raise a ViperError exception:

from dsviper import ViperError, ValueInt8

try:
    v = ValueInt8(200)  # Out of range for int8
except ViperError as e:
    print(str(e))

Output:

[macbook@myapp]:Viper:Value:1:value is not in the range of 'int8'

The message follows the format: [host@process]:Component:Domain:Code:Message

Parsing Error Details

To access individual error fields, use the Error class to parse the exception message:

from dsviper import ViperError, Error, ValueInt8

try:
    v = ValueInt8(200)
except ViperError as e:
    error = Error.parse(str(e))
    if error:
        print(error.component())  # "Viper"
        print(error.domain())  # "Value"
        print(error.code())  # 1
        print(error.message())  # "value is not in the range of 'int8'"
        print(error.hostname())  # "macbook"
        print(error.process_name())  # "myapp"

Error Class Methods

Method

Return

Description

Error.parse(str)

Error or None

Parse an error string into an Error object

error.component()

str

The subsystem (e.g., “Viper”, “Database”)

error.domain()

str

The functional domain (e.g., “Value”, “Type”)

error.code()

int

Numeric error code within the domain

error.message()

str

Human-readable description

error.hostname()

str

Host where error originated

error.process_name()

str

Process name

error.explained()

str

Full formatted error string


Common Error Categories

Type Mismatch Errors

These occur when you provide a value of the wrong type:

from dsviper import Value, Type, TypeVector, ViperError

t_vec = TypeVector(Type.INT64)

try:
    # Trying to create a vector with strings instead of integers
    v = Value.create(t_vec, ["a", "b", "c"])
except ViperError as e:
    print(str(e))  # "...expected type 'long', got 'str'..."

Recovery: Check your input types before calling Viper functions.

Range Validation Errors

These occur when numeric values exceed their type’s range:

from dsviper import ValueInt8, ViperError

# Int8 range: -128 to 127
try:
    v = ValueInt8(200)
except ViperError as e:
    print(str(e))  # "...value is not in the range of 'int8'..."

Integer type ranges:

Type

Range

Int8

-128 to 127

Int16

-32,768 to 32,767

Int32

-2³¹ to 2³¹-1

Int64

-2⁶³ to 2⁶³-1

UInt8

0 to 255

UInt16

0 to 65,535

UInt32

0 to 2³²-1

UInt64

0 to 2⁶⁴-1

Recovery: Validate numeric inputs before creating values, or use larger types.

Optional Unwrap Errors

These occur when you try to unwrap an empty (nil) optional:

from dsviper import Value, Type, TypeOptional, ValueOptional

t_opt = TypeOptional(Type.STRING)
v_empty = ValueOptional(t_opt)  # Creates nil optional

try:
    content = v_empty.unwrap()
except Exception as e:
    print("Cannot unwrap nil optional")

Recovery: Always check is_nil() before calling unwrap():

if not v_empty.is_nil():
    content = v_empty.unwrap()
else:
    content = "default value"

Cast Errors

These occur when you cast a value to an incompatible type:

from dsviper import ValueString, ValueInt64, ViperError

v_str = ValueString("hello")

try:
    v_int = ValueInt64.cast(v_str)
except ViperError as e:
    print(str(e))  # "...expected int64, got string [cast]..."

Recovery: Use type() to check the value’s type before casting:

from dsviper import Type

if v_str.type() == Type.INT64:
    v_int = ValueInt64.cast(v_str)

Key Not Found (Not an Error)

When accessing non-existent keys in databases, Viper does not raise an exception. Instead, get() returns a ValueOptional that is nil:

# db.get() returns ValueOptional, not the value directly
result = db.get(attachment, unknown_key)

# Check if key exists
if result.is_nil():
    print("Key not found")
else:
    value = result.unwrap()

Note: ValueOptional is a Viper container type, unrelated to Python’s typing.Optional. It wraps a value with nil/non-nil semantics and provides is_nil(), unwrap(), and wrap() methods.

Alternatives: Use has() to check key existence before get:

if db.has(attachment, key):
    value = db.get(attachment, key).unwrap()

DSM Parse Errors

These occur when parsing invalid DSM files:

from dsviper import DSMBuilder

builder = DSMBuilder.assemble("invalid.dsm")
report, dsm_defs, defs = builder.parse()

if report.has_error():
    for error in report.errors():
        print(f"Line {error.line()}: {error.message()}")

Note: DSM parsing returns a report object instead of raising exceptions, allowing you to collect multiple errors at once.


Best Practices

1. Catch Specific Errors

Always catch ViperError specifically, not bare Exception:

# Good
try:
    v = ValueInt8(value)
except ViperError as e:
    handle_viper_error(e)

# Bad - catches too much
try:
    v = ValueInt8(value)
except Exception:
    pass

2. Check Before You Leap

For performance-critical code, validate inputs before calling Viper:

# Check range before creating value
def safe_int8(value: int) -> ValueInt8:
    if not (-128 <= value <= 127):
        raise ValueError(f"Value {value} out of int8 range")
    return ValueInt8(value)

3. Use Optionals Correctly

Never assume an optional contains a value:

# Good - check before unwrap
result = db.get(attachment, key)
if not result.is_nil():
    value = result.unwrap()
else:
    value = default

# Bad - will raise if nil
value = db.get(attachment, key).unwrap()

4. Handle DSM Errors Gracefully

Always check the parse report before using definitions:

builder = DSMBuilder.assemble(dsm_path)
report, dsm_defs, defs = builder.parse()

if report.has_error():
    for err in report.errors():
        print(f"Error at line {err.line()}: {err.message()}")
    raise RuntimeError("DSM parsing failed")

# Safe to use defs here

5. Transaction Error Handling

Wrap database transactions with proper error handling:

db.begin_transaction()
try:
    db.set(attachment, key, value)
    db.commit()
except ViperError as e:
    db.rollback()
    raise

Debugging Tips

Enable Verbose Logging

Use Viper’s logging system to trace operations:

from dsviper import LoggerConsole, Logging

# Create a console logger with debug level
logger = LoggerConsole(Logging.LEVEL_DEBUG)
logging = logger.logging()

# Log operations
logging.info("Starting database operation")
logging.error("Operation failed")

Available log levels: LEVEL_ALL, LEVEL_DEBUG, LEVEL_INFO, LEVEL_WARNING, LEVEL_ERROR, LEVEL_CRITICAL.

Inspect Error Fields

When debugging, parse and print all error fields:

except ViperError as e:
    error = Error.parse(str(e))
    if error:
        print(f"Host: {error.hostname()}")
        print(f"Process: {error.process_name()}")
        print(f"Component: {error.component()}")
        print(f"Domain: {error.domain()}")
        print(f"Code: {error.code()}")
        print(f"Message: {error.message()}")

Remote Errors

When using DatabaseRemote or ServiceRemote, errors include the remote host information, helping you identify where the error occurred:

[remote-server@db-process]:Viper:Database:3:Connection timeout

What’s Next