Skip to content
Back to top
View this page

# 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

- Types and Values - Learn the type system

- Collections - Work with containers