Skip to content

Error Handling Guide

NanaSQLite v1.1.0+ provides unified custom exception classes to make error handling more predictable and easier to manage.

Table of Contents

  1. Custom Exception Classes
  2. Exception Hierarchy
  3. Common Error Scenarios
  4. Best Practices
  5. Debugging and Troubleshooting
  6. Async Error Handling
  7. FAQ

Custom Exception Classes

Base Exception

NanaSQLiteError

Base class for all NanaSQLite-specific exceptions.

python
from nanasqlite import NanaSQLite, NanaSQLiteError

try:
    db = NanaSQLite("mydata.db")
    # Some operations
except NanaSQLiteError as e:
    print(f"NanaSQLite error occurred: {e}")

Specific Exceptions

NanaSQLiteValidationError

Raised for invalid input values or parameters.

Common cases:

  • Invalid table or column names
  • Invalid SQL identifiers
  • Parameter type errors
  • validkit-py schema violations (when validator is configured)
python
from nanasqlite import NanaSQLite, NanaSQLiteValidationError

db = NanaSQLite("mydata.db")

try:
    # Invalid table name (starts with number)
    db.create_table("123invalid", {"id": "INTEGER"})
except NanaSQLiteValidationError as e:
    print(f"Validation error: {e}")

Example — validkit-py schema violation:

python
from validkit import v
from nanasqlite import NanaSQLite, NanaSQLiteValidationError

schema = {"name": v.str(), "age": v.int()}
db = NanaSQLite("mydata.db", validator=schema)

try:
    db["user"] = {"name": "Alice", "age": "invalid"}
except NanaSQLiteValidationError as e:
    print(f"Schema violation: {e}")

For installation, coercion, and per-table schema patterns, see the Validation Guide.

NanaSQLiteDatabaseError

Wraps SQLite/APSW database operation errors.

Common cases:

  • Database locked
  • Disk space exhausted
  • File permission errors
  • SQL syntax errors
python
from nanasqlite import NanaSQLite, NanaSQLiteDatabaseError

db = NanaSQLite("mydata.db")

try:
    # Invalid SQL
    db.execute("INVALID SQL STATEMENT")
except NanaSQLiteDatabaseError as e:
    print(f"Database error: {e}")
    # Access original APSW error
    if e.original_error:
        print(f"Original error: {e.original_error}")

NanaSQLiteTransactionError

Transaction-related errors.

Common cases:

  • Attempting nested transactions
  • Commit/rollback outside transaction
  • Closing connection during transaction
python
from nanasqlite import NanaSQLite, NanaSQLiteTransactionError

db = NanaSQLite("mydata.db")

try:
    db.begin_transaction()
    db.begin_transaction()  # Nesting not allowed
except NanaSQLiteTransactionError as e:
    print(f"Transaction error: {e}")

NanaSQLiteConnectionError

Connection creation or management errors.

Common cases:

  • Connection initialization failure
  • Using orphaned child instance

NanaSQLiteClosedError (v1.2.0+)

Raised when attempting an operation on a closed instance or a child instance whose parent has been closed. It is a subclass of NanaSQLiteConnectionError.

Common cases:

  • Operations after calling close()
  • Operations on a table() instance after its parent has been closed
python
from nanasqlite import NanaSQLite, NanaSQLiteClosedError

db = NanaSQLite("mydata.db")
db.close()

try:
    db["key"] = "value"
except NanaSQLiteClosedError as e:
    print(f"Closed error: {e}")

NanaSQLiteLockError

Reserved for future use regarding lock acquisition failures.

NanaSQLiteCacheError

Reserved for future use regarding cache inconsistencies.


Exception Hierarchy

Exception
└── NanaSQLiteError (base class)
    ├── NanaSQLiteValidationError
    ├── NanaSQLiteDatabaseError
    ├── NanaSQLiteTransactionError
    ├── NanaSQLiteConnectionError
    │   └── NanaSQLiteClosedError
    ├── NanaSQLiteLockError
    └── NanaSQLiteCacheError

Since all NanaSQLite exceptions inherit from NanaSQLiteError, you can catch all of them:

python
from nanasqlite import NanaSQLite, NanaSQLiteError

try:
    db = NanaSQLite("mydata.db")
    # Various operations
    db.begin_transaction()
    db["key"] = "value"
    db.commit()
except NanaSQLiteError as e:
    # Catches all NanaSQLite exceptions
    print(f"Error occurred: {e}")

Common Error Scenarios

1. Database Locked

Problem: Multiple processes or threads accessing the database simultaneously.

python
from nanasqlite import NanaSQLite, NanaSQLiteDatabaseError

db = NanaSQLite("mydata.db")

try:
    db["key"] = "value"
except NanaSQLiteDatabaseError as e:
    if "database is locked" in str(e).lower():
        print("Database is locked. Retrying...")
        # Retry logic

Solutions:

  1. Enable WAL mode (enabled by default)
  2. Set busy_timeout
  3. Properly manage transactions
python
db = NanaSQLite("mydata.db", optimize=True)  # WAL mode enabled
db.pragma("busy_timeout", 5000)  # Wait 5 seconds

2. Nested Transactions

Problem: SQLite doesn't support nested transactions.

python
from nanasqlite import NanaSQLite, NanaSQLiteTransactionError

db = NanaSQLite("mydata.db")

try:
    db.begin_transaction()
    # ... some operations ...
    db.begin_transaction()  # Error!
except NanaSQLiteTransactionError as e:
    print(f"Transaction error: {e}")
    db.rollback()

Solution: Check transaction state

python
if not db.in_transaction():
    db.begin_transaction()
# Or use context manager
with db.transaction():
    db["key"] = "value"
    # Auto commit/rollback

3. Closed Connection

Problem: Attempting operations after closing the connection.

python
from nanasqlite import NanaSQLite, NanaSQLiteConnectionError

db = NanaSQLite("mydata.db")
db.close()

try:
    db["key"] = "value"
except NanaSQLiteConnectionError as e:
    print(f"Connection is closed: {e}")

Solution: Use context manager

python
with NanaSQLite("mydata.db") as db:
    db["key"] = "value"
    # Automatically closed

4. Orphaned Child Instances

Problem: Trying to use a child instance (.table()) after the parent connection has been closed.

python
main_db = NanaSQLite("app.db")
sub_db = main_db.table("users")

main_db.close()  # Close parent

try:
    sub_db["key"] = "value"  # Error!
except NanaSQLiteConnectionError as e:
    print(f"Parent connection closed: {e}")

Solution: Manage parent and child scope together.

python
with NanaSQLite("app.db") as main_db:
    sub_db = main_db.table("users")
    sub_db["key"] = "value"

5. Invalid Identifiers

Problem: Identifiers are strictly validated to prevent SQL injection.

python
try:
    db.create_table("my table", {"id": "INTEGER"}) # contains space
except NanaSQLiteValidationError as e:
    print(f"Invalid identifier: {e}")

Solution: Use valid alphanumeric characters and underscores.

python
db.create_table("my_table", {"id": "INTEGER"})

Best Practices

1. Catch Specific Exceptions

python
from nanasqlite import (
    NanaSQLite,
    NanaSQLiteValidationError,
    NanaSQLiteDatabaseError,
    NanaSQLiteConnectionError,
)

db = NanaSQLite("mydata.db")

try:
    db.create_table("users", {"id": "INTEGER", "name": "TEXT"})
    db.sql_insert("users", {"id": 1, "name": "Alice"})
except NanaSQLiteValidationError as e:
    print(f"Invalid input: {e}")
except NanaSQLiteDatabaseError as e:
    print(f"Database error: {e}")
    if e.original_error:
        print(f"Details: {e.original_error}")
except NanaSQLiteConnectionError as e:
    print(f"Connection error: {e}")

2. Use Context Managers

python
# ✅ Recommended
with NanaSQLite("mydata.db") as db:
    db["key"] = "value"
    # Auto-closed even if exception occurs

# ❌ Not recommended
db = NanaSQLite("mydata.db")
try:
    db["key"] = "value"
finally:
    db.close()  # Manual close required

3. Use Transactions for Consistency

python
from nanasqlite import NanaSQLite, NanaSQLiteError

db = NanaSQLite("mydata.db")

try:
    with db.transaction():
        # Withdraw from account A
        db.sql_update("accounts", {"balance": 900.0}, "id = ?", (1,))
        # Deposit to account B
        db.sql_update("accounts", {"balance": 1100.0}, "id = ?", (2,))
        # Auto commit on success
except NanaSQLiteError as e:
    # Auto rollback on exception
    print(f"Transaction failed: {e}")

4. Use Logging

python
import logging
from nanasqlite import NanaSQLite, NanaSQLiteError

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

try:
    db = NanaSQLite("mydata.db")
    db["key"] = "value"
    logger.info("Data saved successfully")
except NanaSQLiteError as e:
    logger.error(f"Error occurred: {e}", exc_info=True)

Debugging and Troubleshooting

Retrieving Error Information

NanaSQLiteDatabaseError holds the original APSW error.

python
from nanasqlite import NanaSQLite, NanaSQLiteDatabaseError

try:
    db = NanaSQLite("mydata.db")
    db.execute("INVALID SQL")
except NanaSQLiteDatabaseError as e:
    print(f"Message: {e}")
    if e.original_error:
        print(f"Original APSW Error: {e.original_error}")
        print(f"Error Type: {type(e.original_error)}")

Checking Transaction State

python
db = NanaSQLite("mydata.db")

print(f"In transaction: {db.in_transaction()}")  # False

db.begin_transaction()
print(f"In transaction: {db.in_transaction()}")  # True

db.commit()
print(f"In transaction: {db.in_transaction()}")  # False

Checking Connection State

python
db = NanaSQLite("mydata.db")
print(f"Is owner: {db._is_connection_owner}")
print(f"Is closed: {db._is_closed}")

sub_db = db.table("users")
print(f"Child is owner: {sub_db._is_connection_owner}")  # False
print(f"Parent closed: {sub_db._parent_closed}")  # False

db.close()
print(f"Child check parent closed: {sub_db._parent_closed}")  # True

Enabling Debug Mode

You can use Python's -v flag or PYTHONVERBOSE environment variable to see module loading and some internal details.

bash
# Windows
$env:PYTHONVERBOSE=1
python your_script.py

# Linux/Mac
PYTHONVERBOSE=1 python your_script.py

Detailed Traceback

python
import traceback
from nanasqlite import NanaSQLite, NanaSQLiteError

try:
    db = NanaSQLite("mydata.db")
    # ... operations ...
except NanaSQLiteError as e:
    print("Error occurred:")
    print(traceback.format_exc())

Async Error Handling

The async version (AsyncNanaSQLite) uses the same exception classes:

python
import asyncio
from nanasqlite import AsyncNanaSQLite, NanaSQLiteError

async def main():
    try:
        async with AsyncNanaSQLite("mydata.db") as db:
            await db.aset("key", "value")
    except NanaSQLiteError as e:
        print(f"Error: {e}")

asyncio.run(main())

FAQ

Q: I frequently encounter "database is locked" errors

Cause: Multiple processes or threads are attempting to write simultaneously, or a long-running transaction is holding the connection.

Solutions:

  1. Check WAL Mode: It's enabled by default, but verify that db.pragma("journal_mode") returns wal.
  2. Set Busy Timeout: Use db.pragma("busy_timeout", 5000) to wait for the lock to be released.
  3. Shorten Transactions: Commit as soon as write operations are done, or keep with db.transaction(): blocks as small as possible.
  4. Exclude from Antivirus: (Windows) Exclude your database files from real-time scans.

Q: Memory usage keeps increasing

Cause: The in-memory cache is accumulating data as you read more keys.

Solutions:

  1. Refresh Cache: Periodically call db.refresh() to clear the memory and free up space.
  2. Use Lazy Loading: Avoid bulk_load=True and only load data as needed.
  3. Recreate Instance: For long-running processes, occasionally closing and reopening the connection can help.

Q: Updates to specific keys aren't being reflected

Cause: Inconsistent connections (e.g., direct manipulation via execute()) are causing a mismatch between the memory cache and the database content.

Solutions:

  1. Use get_fresh(key): Bypass the cache to get the latest data directly from the DB.
  2. Call refresh() after execute(): If you modify data via raw SQL, always call db.refresh(key) to synchronize the cache.

Summary

  • Unified exceptions: All NanaSQLite exceptions inherit from NanaSQLiteError
  • Specific error handling: Catch specific exceptions for appropriate handling
  • Context managers: Automatic resource management
  • Transactions: Maintain data consistency
  • Logging: Track and diagnose errors

Proper error handling enables you to build robust and reliable applications.