SAS Base Programming common mistakes
SAS Base Programming · Understand the traps and how to avoid them.
Declaring a LENGTH statement after the first statement that references the new variable, then wondering why long character values are truncated to $8 or why a variable came out numeric instead of character.
Candidates picture LENGTH as a global declaration like in other languages and place it near the top of the step only by habit, or place it right before the assignment they think it controls. In a DATA step a variable's type and length are fixed the first time the variable is encountered, so a LENGTH that appears later does not resize it, and the compiler issues no dramatic error - you only see truncated values in the output.
What to do instead
Put LENGTH (and ATTRIB) at the very top of the DATA step, before SET/MERGE and before any assignment that creates the variable. When reviewing a step, scan for any LENGTH line that appears below the first mention of that variable name and move it up.
Using a FORMAT statement inside the DATA step to read a raw character date such as '01JAN2023' into a SAS date, or using an informat name in the INPUT statement when the goal is display.
FORMAT and INFORMAT sound interchangeable and both accept the same names (DATE9., MMDDYY10.), so candidates substitute one for the other. The log then shows the character value being converted with a NOTE, and the resulting variable is a character string or a missing numeric rather than the expected date value.
What to do instead
Memorise the direction pair: INFORMAT tells SAS how to read (INPUT and the informat in an INPUT statement), FORMAT tells SAS how to display (FORMAT statement, PUT). To convert, use INPUT(charvar, date9.) for character-to-numeric/date and PUT(numvar, date9.) for numeric-to-character.
Writing a running total as total = total + amount; without a RETAIN statement, then reporting that SAS 'loses' the accumulation and every row shows only the current amount.
Candidates carry over the expectation from other languages that an assigned variable persists. In SAS the program data vector resets non-retained variables to missing at the top of every iteration, so total is missing each time and the expression evaluates to missing.
What to do instead
Either add RETAIN total 0; before the assignment, or use the SUM statement form total + amount; which retains automatically and treats missing amount as zero. Verify by printing the first few and last few observations, not just the first page.
Subsetting the incoming data with IF when a WHERE would do, and then trying to put a newly created variable on the WHERE statement, e.g. where profit > 0; after profit was computed in the same step.
IF and WHERE both look like 'keep only some rows', so the distinction feels cosmetic. In fact WHERE is applied by the engine before the program data vector is built, so it can only see variables that exist in the input data set, while IF is executed per observation after the PDV is populated.
What to do instead
Use WHERE for conditions on input variables (it is faster and reduces I/O) and IF for conditions involving variables created in the step. If a candidate sees 'variable not found' or 'variable not on file' from a WHERE inside a DATA step, the variable is almost always one created in that same step.
Running MERGE without a sorted input or without a BY statement, or assuming MERGE performs a SQL-style join and matches on any common column, so most rows come out with missing values from the other data set.
MERGE is a one-to-one or one-to-many positional match that requires matching BY variables and sorted inputs. Without BY every observation is matched by position, and with an unsorted BY SAS emits a NOTE about the data not being properly sorted but still produces a data set, so the mistake survives to the output.
What to do instead
Always check that both inputs are sorted by exactly the BY variables before the MERGE, watch the log for 'BY variables are not properly sorted', and remember that variables present in both data sets are overwritten by the later one unless renamed or kept selectively. For true lookups across different keys, use PROC SQL or a hash/format lookup.
Writing a chain of IF-THEN statements where an ELSE was intended, so multiple condition blocks execute for one observation and later assignments overwrite earlier ones (or a subsetting IF at the end of the step silently drops rows).
Indentation in the editor makes consecutive IF statements look like branches, and candidates forget that each independent IF is re-evaluated for every observation. There is no error, only a final value that reflects the last true condition.
What to do instead
Use explicit IF ... THEN ...; ELSE IF ... THEN ...; chains with DO/END blocks, and put the subsetting IF (a bare IF with no THEN) as a deliberate, separate statement. When results look wrong, add a PUT statement or a temporary variable to print which branch fired.
Skimming the log for the word ERROR, finding none, and submitting the output - while ignoring NOTEs such as 'Variable X is uninitialized', 'Numeric values have been converted to character', or 'Missing values were generated'.
Candidates are trained to look for the ERROR keyword, and these conditions are reported at NOTE or WARNING level even though they corrupt values. Character-to-numeric conversions in particular produce a missing value and only a NOTE.
What to do instead
Read the log in three passes: NOTEs about uninitialized variables and conversions, then WARNINGs, then ERRORs. Also compare the 'Observations' count in the data set NOTE with the expected row count, and remember the automatic variables N and ERROR for trapping and debugging.
When a step fails with a cascade of syntax errors, jumping to the last error message and editing that line instead of the first one.
A missing semicolon, an unbalanced quote, or a missing %MEND turns the rest of the step into one long invalid token stream, so SAS generates a dozen plausible-looking errors downstream. The last message points at the symptom; the first points at the cause.
What to do instead
Always fix the first error or the first line flagged in the log, then resubmit before touching anything else. Check for a missing RUN or QUIT at the end of the previous step, since a missing terminator makes the next step's statements get swallowed by the previous one.
Submitting a PROC SQL block without QUIT, or opening ODS destinations and never closing them, then being surprised that the following step misbehaves or the final report lacks closing structure.
PROC SQL is an interactive procedure terminated by QUIT rather than RUN, and ODS destinations stay open until ODS CLOSE or an explicit ODS ALL CLOSE. Because nothing fails loudly, candidates assume both are optional style choices.
What to do instead
End every PROC SQL with QUIT; and every ODS block with the matching ODS CLOSE statement (or ODS ALL CLOSE; at the top of the program to reset destinations). Verify with ODS TRACE ON/listing that the expected output objects were actually generated.
Applying a LABEL or FORMAT inside PROC PRINT or PROC MEANS and then expecting the stored data set to carry that format, or using ODS OUTPUT with a guessed object name and getting no data set at all.
A FORMAT statement in a procedure affects only that procedure's output, whereas a FORMAT in the DATA step is written into the data set's descriptor. Likewise ODS OUTPUT needs the exact table name, which candidates assume is the procedure name.
What to do instead
Attach the format with a FORMAT statement in the DATA step (or PROC DATASETS MODIFY) when it must persist. Run ODS TRACE ON before ODS OUTPUT and copy the exact label/name reported, and confirm the ODS SELECT list is not excluding the table.