If you’ve worked with SAS macros, you’ve probably inherited one written by someone else, and had no idea how to use it. Or maybe you’re the macro writer, looking for ways to handle unexpected parameters and make your macro easier for others to use. Good macro error handling isn’t about catching every possible failure. It’s about making failures visible, early, and diagnosable, so you or whoever inherits your code isn’t debugging blind six months from now.
In this post, we’ll explore three practical techniques for making your macros more robust:
To make these techniques concrete, we’ll work through a real example using the SASHELP.CARS table. You can experiment with these techniques and apply them to your own SAS macros.
Throughout this post, we'll enhance the macro below to make it more robust. The macro uses SASHELP.CARS to create a bar chart showing the number of cars by type. When calling the macro, the user inputs one parameter to filter the chart for the origin. Currently a report is generated if the parameter value is Asia, Europe, USA, or All.
What happens if a user inputs a value with incorrect casing, or an origin that does not exist in the dataset, or if they don’t even know the parameter value is supposed to be an origin? Disaster strikes!
Together, let’s add on to this macro to make it more robust, and ready for many users.
/* Define macro */
%macro origin_chart(origin_filter);
%let origin_check = &origin_filter;
/* Print chart for all origins */
%if &origin_check = All %then %do;
title "Number of Cars by Type for All Origins (Ordered by Descending Frequency)";
proc sgplot data=sashelp.cars;
vbar Type /
stat=freq
categoryorder=respdesc
group=Type
groupdisplay=cluster
datalabel
nooutline;
run;
title;
%end;
/* Print chart for specific origin */
%else %do;
/* Title formatting logic */
%if &origin_check = USA %then %do;
%let origin_display = %upcase(&origin_filter);
%end;
%else %do;
%let origin_display = %sysfunc(propcase(&origin_filter));
%end;
title "Number of Cars by Type for &origin_display (Ordered by Descending Frequency)";
proc sgplot data=sashelp.cars(where=(Origin="&origin_check"));
vbar Type /
stat=freq
categoryorder=respdesc
group=Type
groupdisplay=cluster
datalabel
nooutline;
run;
title;
%end;
%mend origin_chart;
/* Sample macro calls */
%origin_chart(All)
%origin_chart(Asia)
%origin_chart(Europe)
%origin_chart(USA)
Select any image to see a larger version.
Mobile users: To view the images, select the "Full" version at the bottom of the page.
The first step in enhancing our macro is to handle various casings of the parameter value. Ensuring that a user can enter a parameter value in any case significantly reduces errors or unexpected results. Currently, if a user called the macro with a value of usa (lowercase), no report is generated and the log prints the note:
This happened because WHERE filters in SAS are case sensitive, and none of the values in Origin match lowercase usa. We can use the %UPCASE macro function to uppercase resolved parameter values in our macro. Additionally, we can compare the resolved value to an all uppercase value.
%if %upcase(&origin_filter) = ALL %then %do;
The %UPCASE macro function ensures any casing of the parameter value all is uppercase and prints a report for all origins.
%else %do;
/* Standardize for validation */
%let origin_check = %upcase(&origin_filter);
/* Validate Origin value */
%if &origin_check = USA or
&origin_check = ASIA or
&origin_check = EUROPE %then %do;
/* Title formatting logic */
%if &origin_check = USA %then %do;
%let origin_display = %upcase(&origin_filter);
%end;
%else %do;
%let origin_display = %sysfunc(propcase(&origin_filter));
%end;
title "Number of Cars by Type for &origin_display (Ordered by Descending Frequency)";
proc sgplot data=sashelp.cars (where=(upcase(Origin)="&origin_check"));
...
The &origin_check macro variable stores the uppercase origin value, which can then be compared to USA, ASIA, or EUROPE.
In the WHERE= dataset option, the UPCASE function converts the values in the Origin column of SASHELP.CARS to uppercase. These values are then compared to the uppercase value stored in &origin_check.
Another best practice when making macros that you or others plan to reuse and share is to make the macro self-documenting. You may have a standard within your organization; however, this example shows allowing help, a question mark (?), or no value to trigger printing how to use the macro to the log.
To do this we can add an additional %IF %THEN %DO block at the beginning of the macro.
/* Self-documentation check: blank, HELP, or ? */
%if %length(%superq(origin_filter)) = 0
or %upcase(%superq(origin_filter)) = HELP
or %superq(origin_filter) = %str(?) %then %do;
%put ------------------------------------------------------------;
%put NOTE: %nrstr(%origin_chart) - Usage Information;
%put ------------------------------------------------------------;
%put NOTE: Generates a bar chart of car counts by Type from sashelp.cars,;
%put NOTE: optionally filtered to a single Origin.;
%put NOTE: ;
%put NOTE: Parameter:;
%put NOTE: origin_filter= ALL, USA, ASIA, or EUROPE (any case).;
%put NOTE: Leave blank, or pass HELP or ? to see this note.;
%put NOTE: ;
%put NOTE: Example calls:;
%put NOTE: %nrstr(%origin_chart(all));
%put NOTE: %nrstr(%origin_chart(usa));
%put NOTE: %nrstr(%origin_chart(europe));
%put ------------------------------------------------------------;
%return;
%end;
Inside of the macro %DO loop, %PUT statements are used to write a description of the macro, how to use it, and sample macro calls.
In the example calls, the macro function %NRSTR is used around the macro call to ensure the macro triggers are seen as text and that the macro does not attempt to resolve and run.
The %RETURN statement terminates execution of the macro. Using it inside of this %DO loop ensures that the macro stops execution after the notes are printed to the log.
Now when help, a question mark, or no value is provided, the following prints to the log:
Finally, we need to expect the unexpected, meaning we can handle a user inputting an unexpected parameter value. To do this, add a final %ELSE %DO block to serve as a “catch all” and print how to use the macro to the log.
%else %do;
%put NOTE: Valid values for origin_filter are Asia, Europe, or USA (any case).;
%put NOTE: Call %nrstr(%origin_chart(help)) for full usage information.;
%put NOTE: Chart was not generated.;
%end;
Good error handling doesn’t mean anticipating every possible way a macro could be misused. As shown above, it comes down to three consistent habits: normalize user input to account for casing issues, document the macro so anyone can discover how to call it without reading the code, and validate parameters so invalid values result in a clear, actionable message.
These same three habits apply to almost any macro you write or inherit. Building them in up front costs a little extra code, but it pays for itself the first time someone — including future you — calls the macro the wrong way and gets a clear answer in the log instead of a silent failure or a cryptic error.
The final program below contains all error-handling techniques. It's ready for handoff to other users and will significantly improve reusability. Happy coding!
/* Define macro */
%macro origin_chart(origin_filter);
/* Self-documentation check: blank, HELP, or ? */
%if %length(%superq(origin_filter)) = 0
or %upcase(%superq(origin_filter)) = HELP
or %superq(origin_filter) = %str(?) %then %do;
%put ------------------------------------------------------------;
%put NOTE: %nrstr(%origin_chart) - Usage Information;
%put ------------------------------------------------------------;
%put NOTE: Generates a bar chart of car counts by Type from sashelp.cars,;
%put NOTE: optionally filtered to a single Origin.;
%put NOTE: ;
%put NOTE: Parameter:;
%put NOTE: origin_filter= ALL, USA, ASIA, or EUROPE (any case).;
%put NOTE: Leave blank, or pass HELP or ? to see this note.;
%put NOTE: ;
%put NOTE: Example calls:;
%put NOTE: %nrstr(%origin_chart(all));
%put NOTE: %nrstr(%origin_chart(usa));
%put NOTE: %nrstr(%origin_chart(europe));
%put ------------------------------------------------------------;
%return;
%end;
/* Print chart for all origins */
%else %if %upcase(&origin_filter) = ALL %then %do;
%put NOTE: No origin_filter specified. Chart generated for ALL origins.;
title "Number of Cars by Type for All Origins (Ordered by Descending Frequency)";
proc sgplot data=sashelp.cars;
vbar Type /
stat=freq
categoryorder=respdesc
group=Type
groupdisplay=cluster
datalabel
nooutline;
run;
title;
%end;
/* Print chart for specific origin */
%else %do;
/* Standardize for validation */
%let origin_check = %upcase(&origin_filter);
/* Validate Origin value */
%if &origin_check = USA or
&origin_check = ASIA or
&origin_check = EUROPE %then %do;
/* Title formatting logic */
%if &origin_check = USA %then %do;
%let origin_display = %upcase(&origin_filter);
%end;
%else %do;
%let origin_display = %sysfunc(propcase(&origin_filter));
%end;
title "Number of Cars by Type for &origin_display (Ordered by Descending Frequency)";
proc sgplot data=sashelp.cars(
where=(upcase(Origin)="&origin_check"));
vbar Type /
stat=freq
categoryorder=respdesc
group=Type
groupdisplay=cluster
datalabel
nooutline;
run;
title;
%end;
%else %do;
%put NOTE: Valid values for origin_filter are Asia, Europe, or USA (any case).;
%put NOTE: Call %nrstr(%origin_chart(help)) for full usage information.;
%put NOTE: Chart was not generated.;
%end;
%end;
%mend origin_chart;
/* Sample macro calls */
%origin_chart(All)
%origin_chart(Asia)
%origin_chart(Europe)
%origin_chart(usa)
%origin_chart()
%origin_chart(help)
%origin_chart(?)
%origin_chart(africa)
Find more articles from SAS Global Enablement and Learning here.
Parameter validation is an important tool for making macros that are easy to use and also easy to create. The late Tom Hoffman created an excellent utility macro for implementing this idea over 30 years ago named %PARMV. You can find an updated version of the macro on github at https://github.com/sasutils/macros/blob/master/parmv.sas.
Using this macro will insure that your error messages are consistently styled and also make it easy to implement common parameter checking features such as automatic case conversion, valid value checking, default values, etc. You can read the source code of the %PARMV() macro to see what else it can, including in the comment block some simple examples.
In your example you will want to use the _VAL= parameter of %PARMV() to pass in the list of valid values. You will also want to use _DEF= parameter to pass in the fact that ALL should be the default value. And leave the _CASE= parameter at its default setting (U for uppercase).
%parmv(origin_filter,_val=all usa asia europe,_def=all)
So using this macro your example program can be reduced to this much simpler definition.
%macro origin_chart
/*-----------------------------------------------------------------------
Generate a bar chart of car counts by Type from sashelp.cars
-----------------------------------------------------------------------*/
(origin /* optional filter to a single Origin value */
);
/*-----------------------------------------------------------------------
Generate a bar chart of car counts by Type from sashelp.cars
Example calls:;
%origin_chart(all)
%origin_chart(usa)
%origin_chart(europe)
-----------------------------------------------------------------------*/
%local parmerr ;
%parmv(origin,_val=all usa asia europe,_def=all)
%if (not &parmerr) %then %do;
title "Number of Cars by Type for &origin (Ordered by Descending Frequency)";
proc sgplot data=sashelp.cars;
%if ALL ne &origin %then %do;
where upcase(Origin)="&origin";
%end;
vbar Type /
stat=freq
categoryorder=respdesc
group=Type
groupdisplay=cluster
datalabel
nooutline
;
run;
title;
%end;
%mend origin_chart;
Example log:
There is another key feature of parameter checking that you did not get to that %PARMV() can help with. In a complex macro that might have multiple parameters the user might have provided more than one invalid parameter value. It is extremely frustrating to have to fix your typos or mistakes one at a time. By using the PARMERR flag variable %PARMV() will allow the coder of the macro to implement checks for all of the parameters and report all of the mistakes on the first call.
Hi @Tom thank you so much for uncovering this hidden gem! That macro is fantastic. For the sake of keeping the example code "simple" I stuck to one parameter, however, knowing the %PARMV() macro also has checks for multiple parameters is even more helpful. Many thanks for commenting and including the example code!
Good overview. Robust error handling is especially important in SAS macros because a macro can technically finish executing while producing incorrect or incomplete output. I find it useful to validate input datasets and parameters early, check &SYSERR and related automatic macro variables after critical procedures, and use clear %PUT messages so failures are easy to trace. Returning a clear status or stopping execution when a required condition fails can make reusable macros much safer in production.
Turn on the MPRINT option to make reading the log (and figuring out what the error is) much easier.
options mprint;
Excellent article, @CarleighJoC! I’ve loved SAS Macro ever since I discovered it way back in the 90s, and have been using the exact same self-documenting technique for a couple of decades now. I find it helpful because I can never remember the details of how to use a macro, even when I wrote it myself 😆
Will you be attending SEASUG this year?
@AgricStatistics I absolutely agree! Great tip to check &SYSERR and automatic variables. %PUT messages are also great along with SYMBOLGEN and the MPRINT option suggested by @PaigeMiller . Thank you both for adding in your macro tricks!
Thanks so much @SASJedi - my passion for learning more about macros stems from you! You wouldn't believe the number of times I've looked something up and one of your blogs or solutions has come to my rescue.
I won't be at SEASUG this year, but I'll be at BASUG in October (using this blog as a starting point for one of my presentations).
We’re so excited you’re coming to BASUG to present in person at our annual SAS Blowout on Oct 30! We’re just putting the finishing touches on the official announcement. Registration opens Tues Sept 22. www.basug.org/events.
Join us to see presentations by @CarleighJoC, Charu Shankar and Jane Eslinger.
I can't wait for the event! @Quentin
Visit the Tips & Tricks page for setup guidance, demos, and practical examples that show how Copilot supports your workflows.
The rapid growth of AI technologies is driving an AI skills gap and demand for AI talent. Ready to grow your AI literacy? SAS offers free ways to get started for beginners, business leaders, and analytics professionals of all skill levels. Your future self will thank you.