Tcl · Module 14
The Three Errors You Will Actually Hit
Almost every Tcl error you meet in your first year is one of three messages. Each names a different phase of execution — parse, substitute, dispatch — and that is the fastest route to the fix.
Foundation18 min readtclerrorsdebuggingquotingwrong-argstroubleshooting
Part I · Module 14 · Chapter 14.7 · The Three Errors You Will Actually Hit
Tcl has a small vocabulary of complaints, and three of them account for the overwhelming majority of what you will see:
can't read "x": no such variable
extra characters after close-brace
wrong # args: should be "report design slack"The useful way to learn them is not as three fixes to memorise. It is to notice that each one names a different phase of the execution model from Module 2 — and the phase tells you where the bug is:
| Message | Phase that failed | So the bug is in |
|---|---|---|
can't read "x" | substitution | a name, or a scope |
extra characters after close-brace | parsing | whitespace or a brace |
wrong # args | dispatch | how the words were grouped |
The rest of this chapter takes them one at a time, and then §7 makes the more important point: the three loud errors are the easy ones.
1. can't read — and its three different meanings
can't read is not one message. The text after the colon distinguishes three genuinely different bugs:
proc show {label script} {
set rc [catch {uplevel 1 $script} r]
puts [format " %-38s %s" $label $r]
}
show {never set} {puts $nosuchvar}
show {typo in the name} {set count 1 ; puts $cuont}
show {set in another proc} {proc p {} { set local 7 } ; p ; puts $local}
show {global not declared} {set ::g 5 ; proc q {} { puts $g } ; q}
show {array vs scalar} {array set a {x 1} ; puts $a}
show {element not present} {array set b {x 1} ; puts $b(y)} never set can't read "nosuchvar": no such variable
typo in the name can't read "cuont": no such variable
set in another proc can't read "local": no such variable
global not declared can't read "g": no such variable
array vs scalar can't read "a": variable is array
element not present can't read "b(y)": no such element in arrayno such variable — the name is not bound in this scope. Four distinct causes produce it, and the message cannot tell them apart:
- It was never set at all.
- The name is misspelled —
cuontforcount. Tcl has no declarations, so a typo on a read is this error, and a typo on a write silently creates a second variable. - It was set in another procedure. Procedure locals are invisible outside, as Module 11 covered.
- It is a global and the procedure never declared it.
set ::g 5at the top level does not make$greadable insideq; that needsglobal gor$::g.
variable is array — the name exists, but as an array, and you used it as a scalar. The fix is an index: $a(x), or array get a for the whole thing.
no such element in array — the array exists and that key does not. Note the message quotes b(y), the full reference, which is how you tell this apart from the scalar case at a glance.
The one-line fix: find the write, not the read. Search for the name; if the only occurrence is the line that failed, it is a typo. If there is a write in another procedure, it is scope.
2. extra characters after close-brace — a whitespace error
This one is almost always a missing space. Tcl's parser splits a command into words on whitespace, so a } immediately followed by anything other than whitespace is not the end of a word:
set x 5
if {$x > 1}{puts "big"}extra characters after close-brace
while executing
"if {$x > 1}{puts "big"}
"
(file "t3.tcl" line 2)The same thing with else glued on:
if {$x > 1} {puts a}else {puts b}extra characters after close-braceThe one-line fix: put a space after every } that ends a word. } {, } else {.
Two silent cousins
The same missing space elsewhere does not error at all, which makes these worse:
set x 5
puts [expr {$x+1}]x6xNo error. [expr {$x+1}]x is one word made of a substitution followed by the literal x, so Tcl concatenated them. If you meant two arguments you got one, and the only symptom is wrong output.
set x 1}
puts "x is <$x>"
puts "length [string length $x]"x is <1}>
length 2A stray close-brace in the middle of a line becomes part of the word. x is now the two-character string 1}, and the failure will surface later, somewhere else, as a wrong number or an arithmetic error.
Its relatives, when the brace never closes
proc p {} {
puts himissing close-braceAnd the variant with a specific, very helpful hint:
proc p {} {
# this { is unbalanced
puts hi
}
pmissing close-brace: possible unbalanced brace in commentTcl counts braces before it decides what is a comment, so an unmatched { inside a # line still has to be matched. The fix is to escape it — \{ — or reword the comment. Tcl naming the possibility explicitly is a kindness; take the hint, because the brace count is otherwise invisible.
3. The newline trap — error #2's cause with error #3's message
This is the Tcl mistake people make most often coming from C or Python, and it is worth its own section because the message is misleading:
set x 5
if {$x > 1}
{
puts "big"
}wrong # args: no script following "$x > 1" argument
while executing
"if {$x > 1}"
(file "t4.tcl" line 2)A newline ends a command in Tcl. if is a command taking arguments, not syntax, so if {$x > 1} on its own line is a complete call to if with exactly one argument — and if needs a script after the condition. The { ... } block on the following lines then tries to execute as a command in its own right.
The one-line fix: the opening brace goes at the end of the previous line. Always.
if {$x > 1} {
puts "big"
}4. wrong # args — read the signature, it contains the answer
The third message is the most informative of the three, because it tells you what was expected:
proc report {design slack} { return "$design: $slack" }
show {too few args to your proc} {report "cpu_top"}
show {too many args} {report "cpu_top" 1.2 extra}
show {builtin: too few} {string length}
show {builtin: too many} {llength a b}
show {proc with a default} {proc r2 {d {s 0.0}} { return "$d/$s" } ; r2} too few args to your proc wrong # args: should be "report design slack"
too many args wrong # args: should be "report design slack"
builtin: too few wrong # args: should be "string length string"
builtin: too many wrong # args: should be "llength list"
proc with a default wrong # args: should be "r2 d ?s?"Four things to read out of these:
- The signature is the parameter list.
should be "report design slack"means two arguments, nameddesignandslack. Count the words after the command name. - Too few and too many give the identical message. It does not tell you which — count.
?s?means optional.should be "r2 d ?s?"is one required parameter and one with a default, sor2 cpu_topis legal and barer2is not. The convention is used for built-ins too.- Built-ins use the same machinery, so
string lengthwith no argument reports its own signature.
The one-line fix: count the arguments you passed against the signature in the message. If they match, the problem is that one of your words is not the word you think it is — which is §5.
5. When a word is not one word
If the count looks right but Tcl disagrees, something split. Here is where that does and does not happen — and the common belief about it is wrong:
proc report {design slack} { return "$design: $slack" }
set n "cpu top"
puts " 1. direct substitution (safe):"
puts " report \$n 1.2 -> [catch {report $n 1.2} e] / $e"
puts " 2. eval on a built string (NOT safe):"
puts " eval \"report \$n 1.2\" -> [catch {eval "report $n 1.2"} e] / $e"
puts " 3. {*} expansion (NOT safe):"
puts " report {*}\$n 1.2 -> [catch {report {*}$n 1.2} e] / $e"
puts " 4. eval with list (safe):"
puts " eval \[list report \$n 1.2\] -> [catch {eval [list report $n 1.2]} e] / $e" 1. direct substitution (safe):
report $n 1.2 -> 0 / cpu top: 1.2
2. eval on a built string (NOT safe):
eval "report $n 1.2" -> 1 / wrong # args: should be "report design slack"
3. {*} expansion (NOT safe):
report {*}$n 1.2 -> 1 / wrong # args: should be "report design slack"
4. eval with list (safe):
eval [list report $n 1.2] -> 0 / cpu top: 1.2The filename version of the same hazard, since paths with spaces are where you will actually meet it:
set p "my dir/out.txt"
puts " llength \$p : [llength $p] <- a 2-element LIST"
puts " file dirname \$p : [file dirname $p]"
puts " as a command arg : [catch {report $p 1.0} e] / $e"
puts " through eval : [catch {eval "report $p 1.0"} e] / $e" llength $p : 2 <- a 2-element LIST
file dirname $p : my dir
as a command arg : 0 / my dir/out.txt: 1.0
through eval : 1 / wrong # args: should be "report design slack"Note the split between rows two and three. file dirname handles the space correctly, and passing the path as an argument is fine — but llength reports 2, because a space is a list separator. So the real rule for a value with a space in it is narrower and more precise than "it becomes two arguments":
- As a command argument: safe.
- As a list: it is two elements. Wrap it with
listbeforelappend-ing it into one. - Through
evalor{*}: it splits. Useeval [list ...].
6. A decision table
| What you see | Phase | Look for |
|---|---|---|
can't read "x": no such variable | substitute | a typo, or a missing global/upvar |
can't read "x": variable is array | substitute | a missing (index) |
can't read "x(y)": no such element | substitute | the key, with info exists x(y) |
extra characters after close-brace | parse | a missing space after } |
missing close-brace | parse | an unclosed block — count from the top |
...possible unbalanced brace in comment | parse | a { inside a # line |
wrong # args: no script following ... | parse | an opening brace on the wrong line |
wrong # args: should be "..." | dispatch | count your words against the signature |
invalid command name "x" | dispatch | a misspelled or not-yet-defined command |
| no error, wrong output | — | §7 |
7. The loud errors are the easy ones
Here is a short script with a real bug, fixed in stages. Version 1:
proc classify {slack} {
if {$slack < 0.0}
{
return "VIOLATED"
}
return "MET"
}
foreach row {{clk 0.42} {data -0.11}} {
puts " [lindex $row 0] -> [classify [lindex $row 1]]"
}wrong # args: no script following "$slack < 0.0" argument
while executing
"if {$slack < 0.0}"
(procedure "classify" line 2)§3's newline trap. Move the brace up. Version 2 — and while editing, the row is passed instead of the field:
proc classify {slack} {
if {$slack < 0.0} {
return "VIOLATED"
}
return "MET"
}
foreach row {{clk 0.42} {data -0.11}} {
lassign $row pin slack
puts " [lindex $row 0] -> [classify $row]"
} clk -> MET
data -> METNo error. Both paths report MET, and one of them is violated. This is the dangerous bug, and it got no message at all. Why it happens:
set slack "data -0.11"
puts " is it a double? : [string is double -strict $slack]"
puts " \$slack < 0.0 : [expr {$slack < 0.0}] <- a STRING compare, and false" is it a double? : 0
$slack < 0.0 : 0 <- a STRING compare, and falseclassify received the whole row, data -0.11, which is not a number. Tcl's comparison operators fall back to string comparison rather than raising, so "data -0.11" < 0.0 is a perfectly legal false. The if took the MET branch for a timing violation.
Version 3 — pass the field, and add a margin column:
foreach row {{clk 0.42} {data -0.11}} {
puts " [lindex $row 0] -> [classify [lindex $row 1]] (margin [format %.2f $margin])"
}can't read "margin": no such variable
while executing
"format %.2f $margin"
("foreach" body line 2)§1, cause one: margin was never set. Version 4, correct:
proc classify {slack} {
if {$slack < 0.0} {
return "VIOLATED"
}
return "MET"
}
foreach row {{clk 0.42} {data -0.11}} {
lassign $row pin slack
puts [format " %-6s %6.2f %s" $pin $slack [classify $slack]]
} clk 0.42 MET
data -0.11 VIOLATEDThe lesson of the progression: versions 1 and 3 announced themselves and took seconds. Version 2 ran clean and produced a plausible, wrong, safety-relevant answer. Everything in this module beyond the three messages — string is double -strict in 14.11, the validation at boundaries, the deliberate error from 14.2 — exists to convert version-2 bugs into version-1 bugs.
8. Exercises
Exercise 1. Which three different messages begin can't read, and what distinguishes them?
Exercise 2. set total 0 then later puts $totl — which error, and where do you look?
Exercise 3. Why does an unbalanced { inside a # comment break the script?
Exercise 4. wrong # args: should be "scale factor ?rounding?" — how many arguments are legal?
Exercise 5. Does myproc $path 1.0, with $path set to my dir/out.txt, raise wrong # args?
Exercise 6. A script produces plausible but wrong numbers and no errors. Which of the three messages helps, and what do you do instead?
Where this fits
Part of the Tcl curriculum.
