Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

MessagePhase that failedSo the bug is in
can't read "x"substitutiona name, or a scope
extra characters after close-braceparsingwhitespace or a brace
wrong # argsdispatchhow 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 array

no 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 — cuont for count. 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 5 at the top level does not make $g readable inside q; that needs global g or $::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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set x 5
if {$x > 1}{puts "big"}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
extra characters after close-brace
    while executing
"if {$x > 1}{puts "big"}
"
    (file "t3.tcl" line 2)

The same thing with else glued on:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if {$x > 1} {puts a}else {puts b}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
extra characters after close-brace

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set x 5
puts [expr {$x+1}]x
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
6x

No 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set x 1}
puts "x is <$x>"
puts "length [string length $x]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
x is <1}>
length 2

A 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc p {} {
    puts hi
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
missing close-brace

And the variant with a specific, very helpful hint:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc p {} {
    # this { is unbalanced
    puts hi
}
p
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
missing close-brace: possible unbalanced brace in comment

Tcl 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set x 5
if {$x > 1}
{
    puts "big"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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, named design and slack. 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, so r2 cpu_top is legal and bare r2 is not. The convention is used for built-ins too.
  • Built-ins use the same machinery, so string length with 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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.2

The filename version of the same hazard, since paths with spaces are where you will actually meet it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 list before lappend-ing it into one.
  • Through eval or {*}: it splits. Use eval [list ...].

6. A decision table

What you seePhaseLook for
can't read "x": no such variablesubstitutea typo, or a missing global/upvar
can't read "x": variable is arraysubstitutea missing (index)
can't read "x(y)": no such elementsubstitutethe key, with info exists x(y)
extra characters after close-braceparsea missing space after }
missing close-braceparsean unclosed block — count from the top
...possible unbalanced brace in commentparsea { inside a # line
wrong # args: no script following ...parsean opening brace on the wrong line
wrong # args: should be "..."dispatchcount your words against the signature
invalid command name "x"dispatcha 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]]"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  clk -> MET
  data -> MET

No 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  is it a double?     : 0
  $slack < 0.0        : 0   <- a STRING compare, and false

classify 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach row {{clk 0.42} {data -0.11}} {
    puts "  [lindex $row 0] -> [classify [lindex $row 1]]  (margin [format %.2f $margin])"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  clk      0.42  MET
  data    -0.11  VIOLATED

The 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.