Error Handling¶
Nadzoring follows a consistent error-handling pattern across all Python APIs: functions never raise exceptions for expected failures (DNS errors, network timeouts, missing PTR records, SSL connection errors). All such failures are returned as structured data with type-safe error fields, making the library safe to use in automation scripts without wrapping every call in try/except.
Only truly unexpected errors (programming mistakes, missing system commands, unsupported operating systems) are allowed to propagate as exceptions.
Type-Safe Error Fields¶
All result dictionaries use Literal types for their "error" field,
defined in module-specific errors.py files. This enables:
IDE autocompletion for error strings
Static type checking (mypy catches typos)
Single source of truth for documentation
from nadzoring.dns_lookup.utils import resolve_with_timer
from nadzoring.dns_lookup.errors import DNSResolveError
result = resolve_with_timer("example.com", "A")
# Type-safe error checking
if result["error"] == "Domain does not exist":
handle_nxdomain()
elif result["error"] == "Query timeout":
retry_with_longer_timeout()
Error literal modules:
nadzoring.dns_lookup.errors— DNS resolution, reverse DNS, tracenadzoring.security.errors— SSL certificates, HTTP headers, email securitynadzoring.network_base.errors— ping, traceroute, WHOIS, port scanningnadzoring.arp.errors— ARP cache retrieval, spoofing detection
DNS Result Pattern¶
Functions that perform DNS lookups (resolve_with_timer, reverse_dns)
return a dictionary with an "error" field typed as DNSResolveError | None
or DNSReverseError | None. Always check it before using other fields:
from nadzoring.dns_lookup.utils import resolve_with_timer
from nadzoring.dns_lookup.errors import DNSResolveError
result = resolve_with_timer("example.com", "A")
if result["error"]:
# Handle the error — result["error"] is type DNSResolveError
print("DNS error:", result["error"])
else:
# Safe to use records and response_time
print(result["records"])
print(f"RTT: {result['response_time']} ms")
Possible error values for resolve_with_timer (see DNSResolveError):
"Domain does not exist"— NXDOMAIN response"No records of requested type"— domain exists but has no records of the requested type"Query timeout"— nameserver did not respond within the timeout"Operation exceeded lifetime timeout"— overall timeout exceeded"Resolver error"— unexpected resolver error
Reverse DNS:
from nadzoring.dns_lookup.reverse import reverse_dns
from nadzoring.dns_lookup.errors import DNSReverseError
result = reverse_dns("8.8.8.8")
if result["error"]:
# result["error"] is type DNSReverseError | None
print("Reverse lookup failed:", result["error"])
# Possible values: "No PTR record", "No reverse DNS",
# "Query timeout", "Invalid IP address", "Resolver error"
else:
print(result["hostname"])
Result Handling Utilities¶
The nadzoring.utils.result module provides helper functions for working
with error-bearing result dictionaries:
from nadzoring.utils.result import is_success, unwrap, unwrap_or
from nadzoring.utils.errors import NadzoringError
result = resolve_with_timer("example.com", "A")
# Check success without accessing error field
if is_success(result):
print(result["records"])
# Raise on error
try:
safe_result = unwrap(result)
print(safe_result["records"])
except NadzoringError as e:
print(f"Operation failed: {e}")
# Provide fallback on error
records = unwrap_or(result, [])
# records is either the original result dict or empty list
Timeout Errors¶
The OperationTimeoutError exception is raised when a lifetime timeout is
exceeded. This is an expected failure — it should be caught and handled
by converting to an error field in the result dict, not allowed to propagate
to the user.
from nadzoring.utils.timeout import TimeoutConfig, timeout_context, OperationTimeoutError
config = TimeoutConfig(lifetime=5.0)
try:
with timeout_context(config):
result = long_running_operation()
except OperationTimeoutError:
result = {"error": "Operation exceeded lifetime timeout"}
When to use: Lifetime timeouts are useful for operations that may hang
indefinitely (e.g., DNS queries to unresponsive servers, socket connections
to firewalled hosts). Phase-specific timeouts (connect/read) are handled by
socket timeouts and do not raise OperationTimeoutError.
Platform differences: On Unix systems, timeout_context uses SIGALRM
which can interrupt blocking system calls. On Windows, the timeout is checked
only after the operation completes (best-effort).
Empty-Result Pattern¶
Some functions return an empty dict (or empty list) when the result cannot be partially valid. This is common for geolocation, WHOIS, or system information commands.
from nadzoring.network_base.geolocation_ip import geo_ip
result = geo_ip("8.8.8.8")
if not result:
print("Geolocation unavailable (private IP, rate-limit, or network error)")
else:
print(result["city"], result["country"])
from nadzoring.network_base.whois_lookup import whois_lookup
from nadzoring.network_base.errors import WHOISError
result = whois_lookup("example.com")
if result.get("error"):
# result["error"] is type WHOISError | None
print("WHOIS lookup failed:", result["error"])
# Possible values: "Command not found", "Query timeout",
# "No information found", "Invalid target"
Security Module Error Types¶
SSL/TLS Certificate Checks (nadzoring.security.errors.SSLCertError):
from nadzoring.security.check_website_ssl_cert import check_ssl_certificate
from nadzoring.security.errors import SSLCertError
result = check_ssl_certificate("example.com")
if result.get("error"):
# Possible values: "Certificate expired", "Certificate not yet valid",
# "Hostname mismatch", "Self-signed certificate", "Connection timeout",
# "SSL handshake failed", "Certificate verification failed",
# "No certificate returned"
print("SSL error:", result["error"])
HTTP Security Headers (nadzoring.security.errors.HTTPHeaderError):
from nadzoring.security.http_headers import check_http_security_headers
from nadzoring.security.errors import HTTPHeaderError
result = check_http_security_headers("https://example.com")
if result.get("error"):
# Possible values: "Request timeout", "Connection refused",
# "SSL verification failed", "Too many redirects", "Invalid URL"
print("HTTP check failed:", result["error"])
Email Security (nadzoring.security.errors.EmailSecurityError):
from nadzoring.security.email_security import check_email_security
from nadzoring.security.errors import EmailSecurityError
result = check_email_security("example.com")
# Errors are collected in result["all_issues"] list
# Each issue is one of: "No SPF record", "No DKIM record",
# "No DMARC record", "SPF lookup timeout", etc.
Network Base Module Error Types¶
WHOIS Lookups (nadzoring.network_base.errors.WHOISError):
from nadzoring.network_base.whois_lookup import whois_lookup
from nadzoring.network_base.errors import WHOISError
result = whois_lookup("example.com")
if result.get("error"):
# Possible values: "Command not found", "Query timeout",
# "No information found", "Invalid target"
print("WHOIS error:", result["error"])
Traceroute (nadzoring.network_base.errors.TracerouteError):
from nadzoring.network_base.traceroute import traceroute
from nadzoring.network_base.errors import TracerouteError
# traceroute returns list of TraceHop objects, errors are logged
# but the function may raise on system-level failures with messages:
# "Permission denied (needs root)", "Command not found",
# "Network unreachable", "DNS resolution failed", "Timeout"
Port Scanning (nadzoring.network_base.errors.PortScanError):
from nadzoring.network_base.port_scanner import scan_ports, ScanConfig
from nadzoring.network_base.errors import PortScanError
# scan_ports returns list of ScanResult, errors are logged internally
# Resolution failures return None for target_ip and are skipped
Geolocation (nadzoring.network_base.errors.GeolocationError):
from nadzoring.network_base.geolocation_ip import geo_ip
result = geo_ip("8.8.8.8")
if not result:
# Empty dict indicates: "API request failed", "Rate limit exceeded",
# "Invalid IP address", or "Private IP address"
print("Geolocation unavailable")
ARP Module Error Types¶
ARP Cache Retrieval (nadzoring.arp.errors.ARPCacheError):
from nadzoring.arp.cache import ARPCache, ARPCacheRetrievalError
from nadzoring.arp.errors import ARPCacheError
try:
cache = ARPCache()
entries = cache.get_cache()
except ARPCacheRetrievalError as e:
# Exception message is one of: "Command not found",
# "Permission denied (needs root)", "Unsupported platform",
# "Failed to parse ARP cache output"
print("ARP cache error:", e)
ARP Spoofing Detection (nadzoring.arp.errors.ARPSpoofingError):
from nadzoring.arp.realtime import ARPRealtimeDetector
from nadzoring.arp.errors import ARPSpoofingError
detector = ARPRealtimeDetector()
try:
alerts = detector.monitor(interface="eth0", count=100)
except RuntimeError as e:
# RuntimeError wraps ARPSpoofingError messages:
# "No network interface specified", "Packet capture failed",
# "No ARP packets captured"
print("Monitoring failed:", e)
Exception Pattern¶
Exceptions are reserved for system-level failures that the caller cannot
reasonably handle inline. All custom exceptions inherit from
nadzoring.utils.errors.NadzoringError.
from nadzoring.arp.cache import ARPCache, ARPCacheRetrievalError
from nadzoring.utils.errors import ARPError, DNSError, NetworkError, NadzoringError
try:
cache = ARPCache()
entries = cache.get_cache()
except ARPCacheRetrievalError as exc:
# System command missing, permission denied, etc.
# Exception message matches ARPCacheError literal
print("Cannot read ARP cache:", exc)
# Catch any Nadzoring error at any granularity
try:
# some operation
pass
except NadzoringError as exc:
print("A Nadzoring operation failed:", exc)
except DNSError as exc:
# Handle only DNS-related errors
print("DNS operation failed:", exc)
Common exception types:
DNSError— base for all DNS errors -DNSResolutionError— resolution failed -DNSTimeoutError— query timed out -DNSDomainNotFoundError— NXDOMAIN -DNSNoRecordsError— record type not presentNetworkError— base for network errors -HostResolutionError— hostname resolution failed -ConnectionTimeoutError— connection timed out -UnsupportedPlatformError— OS not supportedARPError— base for ARP errors -ARPCacheRetrievalError— failed to read ARP cacheValidationError— input validation failed -InvalidIPAddressError— not a valid IP -InvalidDomainError— not a valid domain name -InvalidPortError— port out of range
Command-Line Error Handling¶
When using the CLI, errors are displayed in red and the command exits with a
non-zero status code. Use --quiet to suppress all output except the error
message.
$ nadzoring dns resolve non-existent-domain.example
DNS error: Domain does not exist
$ echo $?
1
$ nadzoring dns resolve -o json non-existent-domain.example
{"error": "Domain does not exist", ...}
$ echo $?
1
Timeout errors in CLI:
When a lifetime timeout is exceeded, the command exits with an error message and a non-zero status code. Phase-specific timeouts (connect/read) are handled within the operation and returned as error fields where applicable.
$ nadzoring dns resolve --timeout 1 slow-domain.example
DNS error: Operation exceeded lifetime timeout
$ echo $?
1
Best Practices for Scripts¶
Always check the error field for DNS/network operations.
Use truthiness checks for functions that return empty dicts/lists.
Use ``is_success()`` helper for cleaner error checking.
Use ``unwrap_or()`` for graceful degradation with defaults.
Catch specific exceptions only for system-level failures.
Use timeout contexts for operations that may hang.
Use the Literal error types for type-safe pattern matching.
Example robust monitoring script with timeout support:
from nadzoring.dns_lookup.utils import resolve_with_timer
from nadzoring.dns_lookup.health import health_check_dns
from nadzoring.dns_lookup.errors import DNSResolveError
from nadzoring.utils.errors import NadzoringError
from nadzoring.utils.result import is_success, unwrap_or
from nadzoring.utils.timeout import TimeoutConfig
def check_domain(domain: str, timeout_sec: float = 30.0) -> dict:
"""Safe domain checker — never raises."""
timeout_config = TimeoutConfig(lifetime=timeout_sec)
result = {
"domain": domain,
"a_record": None,
"health_score": None,
"error": None,
}
# DNS resolution with timeout
a_result = resolve_with_timer(
domain, "A",
timeout_config=timeout_config,
)
if not is_success(a_result):
# Type-safe error handling
error: DNSResolveError | None = a_result.get("error")
result["error"] = f"A record failed: {error}"
return result
result["a_record"] = a_result["records"][0]
# Health check with fallback
try:
health = health_check_dns(domain, timeout_config=timeout_config)
result["health_score"] = health["score"]
except NadzoringError as exc:
# System-level failure (unlikely, but possible)
result["error"] = f"Health check system error: {exc}"
return result
# Use it
data = check_domain("example.com", timeout_sec=10.0)
if data["error"]:
print(f"Check failed: {data['error']}")
else:
print(f"OK: {data['a_record']} score={data['health_score']}")
# Using unwrap_or for graceful degradation
from nadzoring.utils.result import unwrap_or
result = resolve_with_timer("example.com", "A")
records = unwrap_or(result, ["0.0.0.0"]) # Fallback on error
print(f"Resolved to: {records[0]}")
Error Literal Reference¶
DNS Module (nadzoring.dns_lookup.errors):
DNSResolveError:"Domain does not exist","No records of requested type","Query timeout","Operation exceeded lifetime timeout","Resolver error"DNSReverseError:"No PTR record","No reverse DNS","Query timeout","Invalid IP address","Resolver error"DNSTraceError:"Loop detected","Delegation error","No further delegation","Timeout","Domain does not exist","No answer"
Security Module (nadzoring.security.errors):
SSLCertError:"Certificate expired","Certificate not yet valid","Hostname mismatch","Self-signed certificate","Connection timeout","SSL handshake failed","Certificate verification failed","No certificate returned"HTTPHeaderError:"Request timeout","Connection refused","SSL verification failed","Too many redirects","Invalid URL"EmailSecurityError:"No SPF record","No DKIM record","No DMARC record","SPF lookup timeout","DKIM lookup timeout","DMARC lookup timeout"
Network Base Module (nadzoring.network_base.errors):
PingError:"Host unreachable","Timeout","Permission denied (needs root)","Invalid address"TracerouteError:"Permission denied (needs root)","Command not found","Network unreachable","DNS resolution failed","Timeout"WHOISError:"Command not found","Query timeout","No information found","Invalid target"PortScanError:"Connection refused","Timeout","Host unreachable","Invalid port range","Resolution failed"GeolocationError:"API request failed","Rate limit exceeded","Invalid IP address","Private IP address"
ARP Module (nadzoring.arp.errors):
ARPCacheError:"Command not found","Permission denied (needs root)","Unsupported platform","Failed to parse ARP cache output"ARPSpoofingError:"No network interface specified","Packet capture failed","No ARP packets captured"