Error Handling
Scriptling provides comprehensive error handling with Python 3-style exception handling.
Error vs Exception
Scriptling has two distinct types of runtime error conditions:
| Aspect | Error | Exception |
|---|---|---|
| Purpose | Runtime errors surfaced by the interpreter | Explicitly raised conditions |
| Can be caught? | Yes. try/except catches both Errors and Exceptions, inferring the exception type from the error message (see Automatic Exception Type Inference). |
Yes (with try/except) |
| Examples | Type errors, name errors, index/key errors, division by zero | SystemExit, ValueError, user-defined |
| Propagation | Converted into a typed Exception when caught | Propagated for try/except |
Try/Except/Finally
The basic structure for error handling:
try:
# Code that might raise an exception
result = 10 / 0
except ZeroDivisionError:
# Handle division by zero
print("Cannot divide by zero")
finally:
# Always executes (optional)
print("Cleanup code here")Try/Except/Else
The else clause runs only when the try block completes without raising an exception. This is distinct from placing code after the try/except block: the else body is skipped if an exception was caught:
try:
result = int(user_input)
except ValueError:
print("Not a valid number")
else:
# Only runs if no exception was raised
print("Parsed successfully:", result)
finally:
# Always runs regardless
print("Done")A common pattern is to keep the try block minimal and put the success-path logic in else:
try:
data = fetch_data(url)
except Exception as e:
log_error(e)
data = None
else:
process(data) # Only runs when fetch_data() succeededMultiple Exception Types
Handle different error types with separate except blocks:
try:
value = int(user_input)
result = 100 / value
except ValueError as e:
print("Invalid number: " + str(e))
except ZeroDivisionError:
print("Cannot divide by zero")
except Exception as e:
print("Unexpected error: " + str(e))Catching with Variable
try:
raise Exception("something went wrong")
except Exception as e:
print("Error: " + str(e))Bare Except
try:
risky_operation()
except:
print("Error occurred") # Catches any exceptionException Type Hierarchy
Scriptling supports Python 3-style exception type matching:
Exception (base class)
├── ValueError - Invalid values
├── TypeError - Type mismatches
├── NameError - Undefined names
├── ImportError - Library or imported name cannot be imported
├── ZeroDivisionError - Division by zero
├── IndexError - Sequence index out of range
├── KeyError - Dictionary key not found
├── AttributeError - Attribute not found on object
└── ... (more specific types)Built-in Exception Types
| Exception Type | When Raised |
|---|---|
Exception |
Base class for all exceptions |
ValueError |
Invalid value for operation |
TypeError |
Operation on wrong type |
NameError |
Variable/identifier not found |
ImportError |
Library or imported name cannot be imported |
ZeroDivisionError |
Division or modulo by zero |
IndexError |
Sequence index out of range |
KeyError |
Dictionary key not found |
AttributeError |
Attribute not found on object |
OSError |
OS-level errors (file not found, permission denied, etc.) |
RuntimeError |
General runtime errors |
Automatic Exception Type Inference
Scriptling automatically infers exception types from error messages:
try:
x = "string" + 123 # Type mismatch
except TypeError as e:
print("Caught type error") # This works!
try:
x = undefined_variable
except NameError as e:
print("Caught name error") # This works!
try:
import optional_library
except ImportError:
print("Optional library is not available")Raise Statement
Basic Raise
def validate_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age seems unrealistic")
return TrueException Constructors
Built-in exception types can be raised using constructors:
raise Exception("generic error")
raise ValueError("invalid value")
raise TypeError("wrong type")
raise NameError("name not defined")
raise ImportError("module not found")Raise Requires an Exception
The operand of raise must be an Exception instance (or an instance of a subclass). Raising a plain string, number, or other value is rejected with "exceptions must derive from BaseException":
raise ValueError("invalid value") # OK
raise "invalid value" # Error: exceptions must derive from BaseExceptionRe-raising Exceptions
Re-raise an exception after handling:
try:
risky_operation()
except Exception as e:
log_error(e)
raise # Re-raise the same exceptionBare raise outside an except block raises an error:
raise # Error: No active exception to re-raiseRaise with Different Type
Change the exception type while preserving context:
try:
parse_config(data)
except ValueError as e:
raise TypeError("Configuration error: " + str(e))Assert Statement
Test conditions and raise errors when they fail:
# Basic assert - raises AssertionError if condition is False
assert x > 0
# Assert with optional error message
assert x > 0, "x must be positive"
# Common use cases
assert len(data) > 0, "Data cannot be empty"
assert user is not None, "User not found"
assert response.status_code == 200, "Request failed"
# Use in functions for validation
def divide(a, b):
assert b != 0, "Cannot divide by zero"
return a / bException Object Properties
When you catch an exception with as e, you can access its properties:
try:
result = 10 / 0
except Exception as e:
print("Type: " + type(e)) # "EXCEPTION"
print("Message: " + str(e)) # "division by zero"Common Patterns
Safe Dictionary Access
# Option 1: Using try/except
try:
value = data["key"]
except KeyError:
value = default_value
# Option 2: Using get() method (preferred)
value = data.get("key", default_value)Safe List Access
try:
item = items[index]
except IndexError:
item = NoneResource Cleanup with Finally (or with)
Use with when the resource implements __enter__/__exit__:
with open_connection() as conn:
process(conn)
# __exit__ called automatically: no finally neededFall back to try/finally when no context manager is available:
file = None
try:
file = open_file("data.txt")
process_file(file)
except Exception as e:
print("Error: " + str(e))
finally:
if file:
file.close()HTTP Error Handling
import json
import requests
try:
options = {"timeout": 5}
response = requests.get("https://api.example.com/data", options)
if response.status_code != 200:
raise RuntimeError("HTTP error: " + str(response.status_code))
data = json.loads(response.body)
print("Success: " + str(len(data)))
except:
print("Request failed")
data = []
finally:
print("Request complete")Custom Exception Patterns
Creating Custom Error Messages
def validate_user(user):
if not user.get("name"):
raise ValueError("User must have a name")
if not user.get("email"):
raise ValueError("User must have an email")
if "@" not in user["email"]:
raise ValueError("Invalid email format")
return TrueException Chaining
def load_config(path):
try:
data = read_file(path)
return parse_json(data)
except FileNotFoundError:
raise ValueError("Config file not found: " + path)
except JSONParseError as e:
raise ValueError("Invalid config format: " + str(e))SystemExit Exception
The sys.exit() function raises a SystemExit exception that can be caught:
import sys
try:
sys.exit(42)
except Exception as e:
print("Caught: " + str(e)) # "Caught: SystemExit: 42"
# Exit with custom message
sys.exit("Fatal error occurred")Exception Handling in Libraries
When writing libraries, follow these guidelines:
- Document exceptions your functions can raise
- Use specific exception types for different error conditions
- Preserve original exceptions when wrapping errors
- Consider recovery scenarios - can the caller reasonably recover?
# Good library design
def parse_date(date_string):
"""
Parse a date string into components.
Args:
date_string: Date in YYYY-MM-DD format
Returns:
Dict with year, month, day
Raises:
ValueError: If date_string is not valid format
TypeError: If date_string is not a string
"""
if not isinstance(date_string, str):
raise TypeError("date_string must be a string")
# ... parsing logicCommon Pitfalls
Catching Too Broadly
# Bad - catches everything including system exits
try:
some_operation()
except Exception:
pass # Silently ignores all errors
# Good - catch specific exceptions
try:
some_operation()
except (ValueError, TypeError) as e:
log_error(e)
# Handle specific expected errorsSilent Failures
# Bad - silently ignores errors
try:
risky_operation()
except:
pass # What went wrong?
# Good - at least log the error
try:
risky_operation()
except Exception as e:
print("Operation failed: " + str(e))Overly Broad Try Blocks
# Bad - too much code in try block
try:
config = load_config()
connect_database()
process_data()
save_results()
except Exception:
handle_error() # Which part failed?
# Good - narrow try blocks
config = load_config()
connect_database()
try:
process_data() # Just the risky part
except DataError as e:
handle_data_error(e)
save_results()Performance Considerations
Try/Except vs Conditional Checks
# Slower for frequent expected failures
try:
value = dict["key"]
except KeyError:
value = default
# Faster for expected lookups
value = dict.get("key", default)Rule of thumb: Use exceptions for exceptional cases, not for control flow.
For Go Developers
When calling Scriptling from Go:
// Check for Error objects
if obj.IsError(result) {
// Handle fatal error
}
// Check for Exception objects
if ex, ok := result.(*object.Exception); ok {
// Check for SystemExit specifically
if ex.IsSystemExit() {
exitCode := ex.GetExitCode()
// Handle exit
}
}Summary
- Use
try/except/else/finallyfor structured error handling - Use
elseto run code only when no exception was raised - Catch specific exception types when possible
- Use exceptions for exceptional cases, not control flow
- Always preserve original exceptions when wrapping errors
- Document which exceptions your functions can raise
- Add context to exceptions to aid debugging
- Avoid silent failures and overly broad exception handlers
See Also
- Functions - Function definitions
- Python Differences - Exception handling differences