Error Handling
Beamtalk has three error-handling mechanisms:
- Exceptions —
on:do:catches exceptions by class - Result type — explicit ok/error values (no exception thrown)
- DNU —
doesNotUnderstandfires when no method is found
All user-facing errors use #beamtalk_error{} records. Internal errors may
use Erlang's native exception system.
1. Exceptions — on:do:
Wrap potentially-failing code in a block and catch errors with on:do:.
The handler block receives the exception object.
TestCase subclass: Ch12Exceptions
testCatchDivisionByZero =>
result := [10 / 0] on: Exception do: [:e | -1]
self assert: result equals: -1
testDoesNotUnderstand =>
result := [42 perform: #bogusMessage] on: RuntimeError do: [:e | #caught]
self assert: result equals: #caught
testUncaughtExceptionPropagates =>
// When the on:do: class doesn't match, the exception propagates:
self should: [
[42 perform: #bogusMessage] on: TypeError do: [:e | #wrong]
] raise: #does_not_understand
testOnDoWithException =>
r1 := [1 / 0] on: Exception do: [:e | #caught]
self assert: r1 equals: #caught
testErrorMessage =>
// Exception objects expose their message:
text := [42 perform: #bogusMessage] on: RuntimeError do: [:e | e message]
self assert: (text isKindOf: String)
The exception hierarchy
Beamtalk's exception classes form a hierarchy:
Exception
└── Error
├── RuntimeError (does_not_understand, arity_mismatch, etc.)
└── TypeError (type mismatches)
Catching Exception catches everything. Catching RuntimeError catches
only runtime dispatch errors. Use the narrowest match appropriate.
Custom exception classes
Define your own exception classes by subclassing Error:
Error subclass: ValidationError
Signal (raise) a custom exception with signal::
TestCase subclass: Ch12CustomErrors
testSignalCustomError =>
result := [
ValidationError signal: "bad input"
] on: ValidationError do: [:e | e message]
self assert: result equals: "bad input"
testCatchByParentClass =>
result := [
ValidationError signal: "oops"
] on: Error do: [:e | "caught by Error"]
self assert: result equals: "caught by Error"
testCatchByRootException =>
result := [
ValidationError signal: "oops"
] on: Exception do: [:e | "caught by Exception"]
self assert: result equals: "caught by Exception"
testNonMatchingClassDoesNotCatch =>
result := [
[ValidationError signal: "no"] on: TypeError do: [:e | "wrong"]
] on: Exception do: [:e | "correct"]
self assert: result equals: "correct"
testExceptionClassPreserved =>
result := [
ValidationError signal: "test"
] on: Exception do: [:e | e class]
self assert: result equals: ValidationError
ensure: — guaranteed cleanup
ensure: runs its block whether the protected code succeeds or raises
an exception. It returns the value of the protected block (not the
cleanup block).
TestCase subclass: Ch12Ensure
testEnsureReturnsBodyValue =>
result := [42] ensure: [99]
self assert: result equals: 42
testEnsureReturnsBodyValueOnSuccess =>
result := ["hello"] ensure: [nil]
self assert: result equals: "hello"
Use ensure: for cleanup that must always happen:
[file := File open: path] ensure: [file close]
[connection doQuery: sql] ensure: [connection release]
Error propagation patterns
Combine on:do: and ensure: for robust error handling:
// Catch, clean up, and re-raise:
[riskyOperation]
on: Error do: [:e |
logger log: "Failed: " ++ e message.
e signal: e message // re-signal to propagate
]
// Guaranteed cleanup regardless of outcome:
[riskyOperation] ensure: [cleanup]
2. Result type — explicit ok/error values
Result is a value class wrapping either a success or a failure.
Use it when callers should handle errors without exceptions, or when you want
composable pipelines.
Construction:
Result ok: value— success with a valueResult error: reason— failure with a reason (usually a symbol)
TestCase subclass: Ch12Result
testOkResult =>
r := Result ok: 42
self assert: r ok
self deny: r isError
self assert: r value equals: 42
testErrorResult =>
r := Result error: #not_found
self assert: r isError
self deny: r ok
self assert: r error equals: #not_found
testValueOr =>
// Safe extraction with default:
self assert: ((Result ok: 42) valueOr: 0) equals: 42
self assert: ((Result error: #x) valueOr: 0) equals: 0
testValueOrDo =>
// Compute the fallback with a block (gets the error reason):
result := (Result error: #not_found) valueOrDo: [:e | "Error: {e}"]
self assert: result equals: "Error: not_found"
testUnwrap =>
// unwrap returns the value or raises if it's an error:
self assert: (Result ok: 99) unwrap equals: 99
self should: [(Result error: #oops) unwrap] raise: #signal
testMapOnOk =>
// map: transforms the success value:
r := (Result ok: 5) map: [:v | v * 2]
self assert: r value equals: 10
testMapOnError =>
// map: is a no-op on errors:
r := (Result error: #fail) map: [:v | v * 2]
self assert: r isError
self assert: r error equals: #fail
A method that returns Result instead of raising:
Object subclass: Parser
safeParseInt: str =>
[Result ok: str asInteger]
on: Error
do: [:e | Result error: #parse_error]
TestCase subclass: Ch12ParserTest
testSafeParseOk =>
p := Parser new
r := p safeParseInt: "42"
self assert: r ok
self assert: r value equals: 42
testSafeParseError =>
p := Parser new
r := p safeParseInt: "not-a-number"
self assert: r isError
self assert: r error equals: #parse_error
When to use Result vs exceptions
| Situation | Approach |
|---|---|
| Unexpected failures (bugs) | Let exceptions propagate |
| Expected, recoverable errors | Return Result |
| Parsing, validation | Result — caller decides how to handle |
| Resource exhaustion, I/O | Either — depends on API style |
3. doesNotUnderstand (DNU)
When you send a message an object doesn't understand, it raises a
doesNotUnderstand error. This is Beamtalk's equivalent of "method not found".
You can guard against this with respondsTo: before sending:
TestCase subclass: Ch12DNU
testRespondsTo =>
self assert: (42 respondsTo: #isZero)
self deny: (42 respondsTo: #bogusMessage)
testGuardedDispatch =>
obj := 42
result := (obj respondsTo: #isZero)
ifTrue: [obj isZero printString]
ifFalse: ["does not respond"]
self assert: result equals: "false"
testDNUCaught =>
result := [42 perform: #bogusMessage] on: RuntimeError do: [:e | #caught]
self assert: result equals: #caught
4. Raising errors from your own code
Use self error: msg to raise an Error from your code.
Use self subclassResponsibility for abstract methods.
Use self notImplemented for stubs.
Object subclass: Validator
validate: x =>
x isNil ifTrue: [self error: "value must not be nil"]
x < 0 ifTrue: [self error: "value must not be negative"]
x
TestCase subclass: Ch12Validator
testValidOk =>
v := Validator new
self assert: (v validate: 5) equals: 5
testNilRaises =>
v := Validator new
self should: [v validate: nil] raise: #beamtalk_error
testNegativeRaises =>
v := Validator new
self should: [v validate: -1] raise: #beamtalk_error
Summary
Exceptions:
[code] on: Exception do: [:e | handler] — catch by class
[code] on: MyCustomError do: [:e | handler] — catch custom exception
[code] ensure: [always-runs] — guaranteed cleanup
self error: "message" — raise from your code
MyError signal: "message" — raise custom exception
Exception hierarchy:
Exception → Error → RuntimeError / TypeError / YourCustomError
Result:
Result ok: value — success
Result error: reason — failure
r ok / r isError
r value / r error
r valueOr: default
r valueOrDo: [:e | block]
r unwrap
r map: [:v | transform]
DNU guard:
obj respondsTo: #selector
Exercises
1. Safe division. Write a block that divides 10 by a given number, catching
any error and returning nil instead. Test it with 2 (→ 5.0) and 0 (→ nil).
Hint
safeDivide := [:n | [10 / n] on: Exception do: [:e | nil]]
safeDivide value: 2 // => 5.0
safeDivide value: 0 // => nil
2. Result pipeline. Create a Result ok: 5, use map: to double it, then
use map: again to add 1. Unwrap the final result.
Hint
r := (Result ok: 5) map: [:v | v * 2] // Result ok: 10
r2 := r map: [:v | v + 1] // Result ok: 11
r2 unwrap // => 11
map: chains on ok values; on error values it's a no-op.
3. Guard with respondsTo:. Write an expression that sends factorial to a
value only if it responds to that message, otherwise returns "not supported".
Test with 5 and "hello".
Hint
safeFactorial := [:obj |
(obj respondsTo: #factorial)
ifTrue: [obj factorial]
ifFalse: ["not supported"]
]
safeFactorial value: 5 // => 120
safeFactorial value: "hello" // => "not supported"
Next: Chapter 13 — Testing with BUnit