Tcl Part I · Module 14 · Errors and Debugging
error: Raising Your Own
The command is one word and takes a message. What takes thought is when to use it and what to put in the message, because both decisions are made under a bias: stopping a script feels like a failure, and carrying on feels like robustness. It is usually the other way round. A script that stops at the moment something is wrong tells you exactly where to look; one that carries on with a default or an empty string produces a result that is wrong somewhere else entirely, and you debug the symptom rather than the cause.
Foundation11 min readTclPart IErrorserrorValidation
Part I · Module 14 · Chapter 14.2 · error: Raising Your Own
1. The command
proc check_width {w} {
if {![string is integer -strict $w]} {
error "width must be an integer, got '$w'"
}
return $w
}
puts [check_width 32]
check_width "abc"32
width must be an integer, got 'abc'error message raises with completion code 1 and unwinds, exactly as Chapter 14.1 described. Everything after it in the procedure is not executed.
2. Why stopping beats continuing
The temptation is to be forgiving:
proc scale_forgiving {value factor} {
if {![string is double -strict $value]} { set value 0 }
return [expr {$value * $factor}]
}
proc scale_strict {value factor} {
if {![string is double -strict $value]} {
error "value must be a number, got '$value'"
}
return [expr {$value * $factor}]
}
puts "forgiving : [scale_forgiving "n/a" 10]"
if {[catch {scale_strict "n/a" 10} m]} {
puts "strict : $m"
}forgiving : 0
strict : value must be a number, got 'n/a'The forgiving version returned 0, which is a number, which will be summed and averaged and plotted along with the real ones. Nothing downstream can tell it apart from a genuine zero. The strict version stopped at the cause.
3. Where to raise
At the boundary — the point where data enters your control:
proc load_widths {rows} {
set out {}
foreach {name value} $rows {
if {![string is integer -strict $value]} {
error "width for '$name' is not an integer: '$value'"
}
lappend out $name $value
}
return $out
}
puts [load_widths {clk 1 data 32}]
if {[catch {load_widths {clk 1 data wide}} m]} { puts "rejected: $m" }clk 1 data 32
rejected: width for 'data' is not an integer: 'wide'Checking once on the way in means every procedure downstream can assume the data is good — which is Chapter 10.7 §5's "guard at boundaries, not everywhere".
4. A message worth reading
Compare two messages for the same failure:
proc bad_message {w} {
if {![string is integer -strict $w]} { error "invalid width" }
}
proc good_message {name w} {
if {![string is integer -strict $w]} {
error "width for port '$name' must be a positive integer, got '$w'"
}
}
catch {bad_message "wide"} m1
catch {good_message data "wide"} m2
puts "bad : $m1"
puts "good : $m2"bad : invalid width
good : width for port 'data' must be a positive integer, got 'wide'The first tells you a width was invalid. The second tells you which one, what was wrong with it, and what was expected — which is usually enough to fix it without opening the script.
5. Three things a good message contains
- The offending value, quoted so whitespace and emptiness are visible.
- The expectation — what would have been acceptable.
- Enough context to locate it — which field, which file, which line.
proc parse_setting {lineno line} {
set i [string first "=" $line]
if {$i < 0} {
error "line $lineno: expected 'key = value', got '$line'"
}
set key [string trim [string range $line 0 [expr {$i-1}]]]
if {$key eq ""} {
error "line $lineno: empty key in '$line'"
}
return $key
}
foreach {n l} {1 "width = 32" 2 "nonsense" 3 " = 5"} {
if {[catch {parse_setting $n $l} r]} {
puts " $r"
} else {
puts " line $n ok: $r"
}
} line 1 ok: width
line 2: expected 'key = value', got 'nonsense'
line 3: empty key in ' = 5'Quoting the value matters: got '' and got ' ' look different, and got alone looks like a truncated message.
6. error's optional arguments
error message ?info? ?code? takes two more arguments, both of which you will use rarely and should recognise.
The second replaces the stack trace that would otherwise be recorded:
catch {error "the message" "a custom trace line"} m
puts "message : $m"
puts "errorInfo : $::errorInfo"message : the message
errorInfo : a custom trace lineThe third sets errorCode, the machine-readable classification from Chapter 14.1 §7:
catch {error "no such port" "" {CONFIG BADPORT clk}} m
puts "message : $m"
puts "errorCode : $::errorCode"message : no such port
errorCode : CONFIG BADPORT clkThat lets a caller branch on what went wrong without matching English text:
proc lookup {name} {
if {$name eq "missing"} {
error "no such port: $name" "" {CONFIG BADPORT}
}
if {$name eq "broken"} {
error "port is malformed: $name" "" {CONFIG MALFORMED}
}
return "ok: $name"
}
foreach n {clk missing broken} {
if {[catch {lookup $n} msg]} {
switch -- [lindex $::errorCode 1] {
BADPORT { puts " $n -> not found, skipping" }
MALFORMED { puts " $n -> malformed, needs fixing" }
default { puts " $n -> unexpected: $msg" }
}
} else {
puts " $n -> $msg"
}
} clk -> ok: clk
missing -> not found, skipping
broken -> malformed, needs fixing7. return -code error inside a procedure
Chapter 14.1 §5 showed these are equivalent:
proc with_error {} { error "raised with error" }
proc with_return {} { return -code error "raised with return -code" }
foreach p {with_error with_return} {
catch {$p} m
puts " $p -> $m"
} with_error -> raised with error
with_return -> raised with return -codePrefer error for ordinary validation — it is shorter and says what it does. return -code error earns its place when you are re-raising something you caught and want to control the options, which Chapter 14.6 covers.
8. A worked example
A validator that raises with good messages at the boundary:
proc validate_port {spec} {
if {[llength $spec] != 3} {
error "port spec needs 3 fields (name dir width), got [llength $spec]: '$spec'"
}
lassign $spec name dir width
if {$name eq ""} {
error "port name must not be empty in '$spec'"
}
if {$dir ni {in out inout}} {
error "port '$name' has bad direction '$dir', expected in, out or inout"
}
if {![string is integer -strict $width]} {
error "port '$name' width must be an integer, got '$width'"
}
if {$width < 1} {
error "port '$name' width must be at least 1, got $width"
}
return [dict create name $name dir $dir width $width]
}
foreach spec {
{clk in 1}
{data out 32}
{bad sideways 8}
{oops in}
{x in 0}
{"" in 1}
} {
if {[catch {validate_port $spec} r]} {
puts " REJECT $r"
} else {
puts " ACCEPT [dict get $r name] ([dict get $r width] bits)"
}
} ACCEPT clk (1 bits)
ACCEPT data (32 bits)
REJECT port 'bad' has bad direction 'sideways', expected in, out or inout
REJECT port spec needs 3 fields (name dir width), got 2: 'oops in'
REJECT port 'x' width must be at least 1, got 0
REJECT port name must not be empty in '"" in 1'Every rejection names the port, the offending value and the expectation. None of them required the reader to open the source.
9. Exercises
Exercise 1. What does error "message" do to the completion code and the call stack?
Exercise 2. Why is substituting a default for an invalid number often worse than raising?
Exercise 3. Name the three things a good error message should contain.
Exercise 4. Why should the offending value be quoted in the message?
Exercise 5. What do error's second and third arguments set?
Exercise 6. When is errorCode worth setting?
Answers
1. It completes with code 1 and unwinds the call stack until something catches it, or the script stops. Nothing after the error in that procedure runs.
2. Because the default is indistinguishable downstream from a real value. A 0 substituted for n/a gets summed, averaged and reported as though it were measured — so the failure surfaces as a wrong number somewhere else, rather than as a stop at the cause.
3. The offending value, the expectation, and enough context to locate it — which field, line or file.
4. So that an empty value or one made of whitespace is visible. got '' and got ' ' are distinguishable; got alone looks like the message was cut off.
5. The second replaces the recorded stack trace (errorInfo); the third sets errorCode, the machine-readable classification.
6. When a caller needs to distinguish failures programmatically — typically in a library other people call, where matching on your English wording would break the moment you reword it.
10. Remember
error messageraises with code 1 and unwinds; nothing after it in that procedure runs.- Stopping at the cause beats continuing with a substituted value, which is indistinguishable downstream.
- Raise at the boundary so everything after it can assume the data is good.
- A good message names the value, the expectation and the location.
- Quote the value so empty and whitespace-only inputs are visible.
error msg info codesets the stack trace and the machine-readableerrorCode.- Branch on
errorCode, not on message text, when a caller must distinguish failures. errorandreturn -code errorare equivalent; prefererrorfor validation.
Where this fits
Part of the Tcl curriculum.
