What this is, and what it is not
Ask a chatbot for an AutoLISP routine and you get back something that looks finished. Correct indentation, sensible variable names, a friendly message at the end. It looks like code that has been through a review.
So let us actually put it through one.
Below is a routine of the kind these tools produce for a steel detailing job — tag the selected blocks with their block name, on a text layer. It is the sort of thing you would ask for on a Tuesday afternoon. Then we mark it up the way you would mark up a junior’s shop drawing, against Autodesk’s own documentation and the published AutoLISP canon.
One thing to be straight about first: this is a code review, not a test. Nothing here was executed in AutoCAD. Every defect called out below is a documentation defect — the code contradicts something Autodesk or Lee Mac has written down — which is precisely the class of problem you can catch by reading, before it ever touches a drawing. There is a second class that only a live session finds, and we flag one of those at the end rather than pretend otherwise.
The routine
(defun c:BEAMTAG ()
(setq ss (ssget '((0 . "INSERT"))))
(setq n 0)
(setvar "clayer" "TEXT-TAGS")
(setvar "osmode" 0)
(while (< n (sslength ss))
(setq ent (ssname ss n))
(setq obj (vlax-ename->vla-object ent))
(setq nm (vla-get-Name obj))
(setq ins (vlax-get obj 'InsertionPoint))
(command "._TEXT" ins 2.5 0 nm)
(setq n (1+ n))
)
(princ (strcat "\nTagged " (itoa n) " beams."))
)
Read it quickly and it is fine. The selection filter is right, the loop is right, the arithmetic is right. Nothing here is stupid. That is the problem — the defects are all in what is absent, and absence is invisible on a fast read.
The red pen
1. Nothing is declared local
The argument list is empty: (defun c:BEAMTAG (). Autodesk states the consequence plainly — “All variables when they are initially declared are global” — so ss, n, ent, obj, nm and ins all leak into the drawing session and stay there.
Autodesk’s own warning on what that costs:
However, if all or many of your variables are global it becomes increasingly possible that you could end up changing the value of a variable so it is incompatible with another function. This can lead to unpredictable behavior and it can be very difficult to identify the source of a problem.
A variable called n is not a name. It is a collision waiting for the next routine you load.
The fix is the slash-delimited list, and there is a spacing rule that trips people up because it is invisible: “Be certain there is at least one space between the slash and each local variable.” (defun c:BEAMTAG (/ss n) is not the same thing as (defun c:BEAMTAG ( / ss n ).
Autodesk also offers a tip that complicates the usual advice, and it explains why generated code is missing this more than anything else:
Tip: Do not make your variables local until after you have done most of the debugging for your function. By not declaring your variables as local right away, you can check the last values assigned to a variable after the function has finished.
Declaring locals is a finishing step. It happens at the end of a debugging phase. A routine that arrived in one shot never had a debugging phase, so it never got finished. That single observation explains most of what follows.
2. No error handler — and Esc counts as an error
There is no *error* function. Lee Mac on why that matters more than it sounds:
what most users don’t realise is that thumping the Esc key during program execution is also considered an error and will hence abort the program instantaneously
The routine changes the current layer and the object snaps on lines 4 and 5. It never puts them back at all — but even if it did, on the last line, Esc during the loop would skip that line entirely.
Autodesk’s instruction: “Before defining your own *error* function, save the current contents of *error* so that the previous error handler can be restored upon exit.”
The spelling trap. Autodesk’s cancel test is:
(if
(or
(= msg "Function cancelled")
(= msg "quit / exit abort")
)
(princ)
(princ (strcat "\nError: " msg))
)
That is “Function cancelled“, British double L, and “quit / exit abort” with spaces around the slash. A handler testing for the American “canceled” does not match, and prints a spurious error every time somebody presses Esc. It is a one-character defect that no compiler will ever tell you about.
3. Two system variables changed and never restored
This is the one that leaves the building. Autodesk gives every system variable a “Saved in” field, and that field is the blast radius:
| Line | Variable | Saved in | Consequence |
|---|---|---|---|
(setvar "clayer" "TEXT-TAGS") |
CLAYER | Drawing | Saved into the DWG. Travels to whoever opens it next. |
(setvar "osmode" 0) |
OSMODE | Registry | Survives closing and reopening AutoCAD. |
Neither is restored anywhere in the routine. CLAYER is the worse of the two: nobody checks the current layer on a drawing check, so it goes out with the issue.
There is also a live hazard in that CLAYER line on its own. It assumes a layer called TEXT-TAGS exists. If it does not, setvar is handed a value it cannot accept, and Autodesk documents the result: “Values supplied as arguments to setvar must be of the expected type. If an invalid type is supplied, an AutoLISP error is generated.” The routine falls over on line 4 — on a drawing that does not happen to have your layer.
4. Zeroing OSMODE, when Autodesk documents something better
(setvar "osmode" 0) is near-universal practice, in generated code and in human code. Autodesk has a note on the OSMODE page that almost nobody acts on:
Note: Developers creating custom routines can use the 16384 bitcode to temporarily suppress the current running object snap settings without losing the original settings.
Set the suppress bit — (setvar 'osmode (logior osm 16384)) — rather than zeroing the variable, and a routine that dies before it restores has not destroyed the user’s snap selection. Given OSMODE is saved in the registry, that difference is the difference between an annoyance and a setting somebody has to rebuild from memory.
This is worth dwelling on, because it is the clearest example of what these tools actually do. The model did not invent (setvar "osmode" 0). It learned it from us — from thirty years of published LISP that does exactly that. It is reproducing the profession’s habits, including the ones the vendor documented a better answer for.
5. No undo group
Tag forty beams and the user gets forty undos. Autodesk explains it better than we can:
Each command executed with the command and command-s functions explicitly creates its own UNDO group. If a user enters U (or UNDO) at the AutoCAD Command prompt after running an AutoLISP routine, only the last command will be undone. Additional uses of UNDO will step backward further through the commands used in that routine. Users of your routine will expect that all of the operations that it performs can be undone in a single operation, instead of having to undo multiple operations to get back to the previous state of the drawing.
Two lines fix it: (command "._UNDO" "_Begin") and (command "._UNDO" "_End"). Note Autodesk’s token order — dot first, then underscore. Plenty of forum code writes _.UNDO; the two prefixes are independent and both orderings work, but only one of them is a quote from the documentation.
6. ActiveX used without loading it
Lines 8 to 10 call vlax-ename->vla-object, vla-get-Name and vlax-get. There is no (vl-load-com) anywhere. Autodesk:
AutoLISP code that includes calls to vla-, vlax-, or vlr- functions should always begin with a call to vl-load-com to ensure that the code will run; it should not be left up to the user to load the extensions. If your application does not call vl-load-com, the application may fail.
Note “may fail”, not “will fail” — and that hedge is the whole reason this defect survives. If anything else in the session already loaded the extensions, this routine works by luck. It passes on the machine it was written on and falls over on a colleague’s fresh session. Autodesk’s “it should not be left up to the user to load the extensions” is the direct answer to “but it worked when I tried it”.
There is a portability cost too. Autodesk’s vl-load-com page gives its supported platforms as “Windows only; not available on Mac OS or Web”. Reaching for ActiveX to read a block name makes this a Windows routine. For this particular job it buys nothing — (cdr (assoc 2 (entget ent))) gets the block name from the entity data, works everywhere, and needs no extensions at all.
7. No guard on an empty selection
(ssget) returns nil if the user presses Esc or selects nothing, and (sslength nil) is an error. The routine has already changed the layer and the snaps by that point. This is the defect that turns a harmless mis-click into the mess described in section 3.
8. The return value prints twice
The last line is (princ (strcat "\nTagged " ...)). That prints the string — and then returns it, so AutoCAD prints it again. Autodesk calls the fix “exiting quietly”: end with a bare (princ).
An awkward aside: Autodesk’s own example
Before this reads as a lecture, it is worth looking at what sits on Autodesk’s command-s reference page as the recommended pattern for using commands inside an error handler:
(defun my_err(s)
(prompt "\nERROR: mycmd failed or was cancelled")
(setvar "clayer" old_clayer)
(command-s "._undo" "_e")
(setq *error* mv_oer)
)
(defun c:mycmd ()
(setq old_err *error*
*error* my_err
old_clayer (getvar "clayer")
)
...
)
Read it the way we just read the generated routine. old_err is assigned and never used. mv_oer is used and never assigned anywhere on the page. And c:mycmd has an empty argument list with no slash, so old_err, old_clayer and the rest all leak globally — the exact defect from section 1, in the vendor’s own published sample, on a page in the same documentation set that tells you not to do it.
This is not a gotcha. It is the point. Production hygiene is fiddly enough that everybody drops a stitch, including the people who wrote the manual. Which is the argument for checking code rather than trusting its author — human, vendor, or model.
The corrected routine
(defun c:BEAMTAG ( / *error* osm clay und ss n ent nm ins )
(defun *error* ( msg )
(if clay (setvar 'clayer clay))
(if osm (setvar 'osmode osm))
(if und (command-s "._UNDO" "_End"))
(if (not (member msg '("Function cancelled" "quit / exit abort")))
(princ (strcat "\nError: " msg))
)
(princ)
)
(if (setq ss (ssget '((0 . "INSERT"))))
(progn
(command "._UNDO" "_Begin")
(setq und t
clay (getvar 'clayer)
osm (getvar 'osmode))
(setvar 'osmode (logior osm 16384))
(if (not (tblsearch "LAYER" "TEXT-TAGS"))
(command "._-LAYER" "_Make" "TEXT-TAGS" "")
)
(setvar 'clayer "TEXT-TAGS")
(setq n 0)
(while (< n (sslength ss))
(setq ent (ssname ss n)
nm (cdr (assoc 2 (entget ent)))
ins (cdr (assoc 10 (entget ent))))
(command "._TEXT" ins 2.5 0 nm)
(setq n (1+ n))
)
(setvar 'clayer clay)
(setvar 'osmode osm)
(command "._UNDO" "_End")
(setq und nil)
(prompt (strcat "\nTagged " (itoa n) " blocks."))
)
(prompt "\nNothing selected.")
)
(princ)
)
Four things about the corrections that are easy to get wrong:
- Every restore is guarded.
(if clay (setvar 'clayer clay)), not(setvar 'clayer clay). The error can fire beforeclaywas ever set, and handingsetvara nil raises a further AutoLISP error — inside the error handler, which is a genuinely unpleasant place to be. - Every restore appears twice — in the handler and on the normal exit path. Belt and braces, and it is what Lee Mac’s canonical example does.
- The handler uses
command-s, notcommand. Autodesk: “Whenever an AutoLISP expression evaluation begins, the AutoLISP engine assumes that the command function will not be allowed within an *error* handler.” The documented ways out are substitutingcommand-sor doing explicit*push-error-using-command*bookkeeping. The first is simpler. - ActiveX is gone entirely. Block name and insertion point both come from
entget, so the routine now runs on AutoCAD, AutoCAD LT 2024+, Mac and web.
Note also what did not need fixing. The selection filter, the loop, the counter, the string building — the logic was right the first time. That is consistent with the whole public record on this: the complaints on the forums are almost never “it got the logic wrong”.
The thing a review cannot catch
Honesty requires one more note. Both versions call (command "._TEXT" ins 2.5 0 nm), supplying a height of 2.5. If the current text style has a fixed height, AutoCAD does not prompt for height — so the 2.5 is consumed by the rotation prompt, the block name lands in the rotation, and the whole sequence derails.
Whether that happens depends on the drawing, not on the code. No amount of reading finds it. That is the boundary of this exercise: a review against the documentation catches the hygiene, and a real drawing catches the rest. Both passes are needed, and only one of them can be done in ninety seconds at your desk.
The checklist
Run a generated routine past this before it goes near a live file.
| Check | Look for | If missing |
|---|---|---|
| Locals declared | ( / var var ) with a space after the slash |
Variables leak into the session |
*error* handler |
Declared in the local list | Esc leaves the drawing mid-change |
| Cancel test spelling | “Function cancelled” — double L | Spurious error on every Esc |
| Restores guarded | (if var (setvar ...)) |
Error inside the error handler |
| Restores in both places | Handler and normal exit | Half the exits leak state |
| Undo group | ._UNDO Begin / End |
User gets N undos, not one |
| OSMODE handling | Bit 16384, not 0 |
User’s snap settings destroyed |
vl-load-com |
Present if any vla-/vlax-/vlr- |
Works by luck; fails on a fresh session |
| ActiveX necessary? | Could entget do it? |
Needlessly Windows-only |
| Empty selection guarded | (if (setq ss (ssget ...)) ...) |
Crash after state was changed |
| Layers assumed | tblsearch before setvar 'clayer |
Fails on drawings without your layer |
| Quiet exit | Bare (princ) last |
Return value printed twice |
Twelve checks, all of them readable off the page. None of them require running anything, and none of them are about whether the model is clever. They are about whether the routine was finished.
For the wider pattern behind these defects, see what actually breaks in AI-generated AutoLISP. For the one failure mode reading cannot reliably catch — invented function names — there is a paste-in function allowlist. And the routines we actually ship, written the long way, are on the AutoLISP hub.
References
- About Local and Global Variables (AutoLISP) — Autodesk AutoLISP Developer’s Guide, AutoCAD 2024. “All variables when they are initially declared are global”; the debugging tip.
- To declare local variables (AutoLISP) — Autodesk AutoLISP Developer’s Guide, AutoCAD 2024. The space-after-the-slash requirement.
- About Using the *error* Function (AutoLISP) — Autodesk AutoLISP Developer’s Guide, AutoCAD 2024. Cancel-string test; saving the previous handler.
- Error Handling — Lee Mac. The localised handler, the guarded restore, and Esc as an error.
- About Undoing Changes Made by a Routine (AutoLISP) — Autodesk AutoLISP Developer’s Guide, AutoCAD 2024.
- command-s (AutoLISP) — Autodesk AutoLISP Reference, AutoCAD 2024. Why
commandis not allowed inside*error*; themy_err/c:mycmdexample. - OSMODE (System Variable) — Autodesk, AutoCAD 2024. Saved in: Registry; the 16384 suppress bitcode.
- CLAYER (System Variable) — Autodesk, AutoCAD 2024. Saved in: Drawing.
- About System and Environment Variables (AutoLISP) — Autodesk AutoLISP Developer’s Guide, AutoCAD 2024. Invalid type supplied to
setvargenerates an AutoLISP error. - About Loading Extended AutoLISP Functions — Autodesk AutoLISP Developer’s Guide, AutoCAD 2024. Why
vl-load-commust come first. - vl-load-com (AutoLISP/ActiveX) — Autodesk AutoLISP Reference, AutoCAD 2024. Supported platforms: Windows only.
- About Exiting a Function Quietly (AutoLISP) — Autodesk AutoLISP Developer’s Guide, AutoCAD 2024.
- AutoLISP for AutoCAD: Tutorials and 139 Free Routines — blog.draftsperson.net.