Using @error responsibly in Sass
Since Ruby Sass 3.4 and LibSass 3.1, it is possible to use the @error
directive. This directive, similar to @warn
, intends to kill the execution process and display a custom message to the current output stream (likely the console).
Needless to say this feature is greatly helpful when building functions and mixins involving a bit of Sass logic, in order to control author’s input and throw an error in case anything goes wrong. You have to concede this is better than letting the compiler fail miserably, isn’t it?
Everything is great. Except Sass 3.3 is still broadly used. Even Sass 3.2 at some places. Updating Sass is not always an easy task especially on large projects. Sometimes, spending time and budget on updating something that works fine is not an option. For those older versions, @error
means nothing and is treated as a custom at-directive, which is perfectly allowed in Sass for forward-compatibility reasons.
So does that mean we can’t use @error
unless we decide to only support recent Sass engines? Well, you can imagine there is a way, hence this article.
What’s the idea?
The idea is simple: if @error
is supported, we use it. Else, we use @warn
. Although @warn
does not prevent the compiler from going any further, so we might want to trigger a compilation error after warning so that the compilation crashes for good. Enjoy it, it’s not that often that you can recklessly crash something.
That means we need to wrap the whole thing in a mixin, let’s call it log(..)
. We could use it like this:
@include log('Oops, something is wrong with what you just did!');
You gotta admit, that’s pretty rad, isn’t it? Okay, enough bragging, let’s build it.
Building the logger
So our mixin works exactly like @error
or @warn
since it’s nothing but a wrapper. Thus, it only needs a single argument: the message.
@mixin log($message) { ... }
You might be asking yourself how we are going to check for @error
support. At first, I came up with a hacky solution involving version sniffing, but that was terrible. Also, I completely overlooked the fact that Sass core designers are smart people who thought about this whole thing and introduced a at-error
key for feature-exists(..)
function, returning whether or not the feature is supported.
@mixin log($message) {
@if feature-exists('at-error') == true {
@error $message;
} @else {
@warn $message;
}
}
If you are a patch note reader, you might know that the feature-exists(..)
function has only been introduced in Sass 3.3. It does not cover Sass 3.2! Well, that’s part true. In Sass 3.2, feature-exists('at-error')
gets evaluated as a string which is truthy. By adding == true
, we make sure that Sass 3.2 is not entering this condition, and moves to the @warn
version.
So far, so good. Although we have to trigger a compilation error after warning. How are we going to do it? Well, there are plenty ways to crash Sass, but ideally we’d want something that you can recognize. Eric Suzanne came up with an idea a while back: calling a function without a @return
statement is enough to crash. This pattern is often called a noop, for no-operation. Basically it’s a blank function, doing nothing. Because of the way Sass works, it crashes the compiler. Which is good!
@function noop() {}
Last but not least thing about this function, how are we going to call it? Sass functions can only be called at specific locations. There are several ways available to us:
- A dummy variable, e.g:
$_: noop()
- A dummy property, e.g:
crash: noop()
- An empty condition, e.g:
@if noop() {}
- And you can probably find several other ways to call this function.
I would like to warn you against using $_
as it is a variable commonly used in Sass libraries and frameworks. While it might not be a problem in Sass 3.3+, in Sass 3.2, this would overwrite any global $_
variable, which would lead to hard to track issues in some cases. Let’s go with the empty condition thing, as it makes sense when paired with a noop. A noop condition for a noop function.
@mixin log($message) {
@if feature-exists('at-error') == true {
@error $message;
} @else {
@warn $message;
// Because functions cannot be called anywhere in Sass,
// we need to hack the call in a dummy condition.
@if noop() {}
}
}
Alright! Let’s test this with our previous code:
@include log('Oops, something is wrong with what you just did!');
Here is LibSass:
message:
path/to/file.scss
1:1 Oops, something is wrong with what you just did!
Details:
column: 1
line: 1
file: /path/to/file.scss
status: 1
messageFormatted: path/to/file.scss
1:1 Oops, something is wrong with what you just did!
Here is Sass 3.4:
Error: Oops, something is wrong with what you just did!
on line 1 of path/to/file.scss, in `log'
Use --trace for backtrace.
Here is Sass 3.2 and 3.3 (output is a wild guess as I can’t test those versions in my terminal easily anymore):
WARNING: Oops, something is wrong with what you just did!
on line 1 of path/to/file.scss, in `log'
ERROR: Function noop finished without @return
on line 1 of path/to/file.scss, in `log'
Use --trace for backtrace.
That seems to do the trick! In any engine, even old ones, the compiler exits. On those which support @at-error
, they get the error message right away. On those which don’t, they get the message as a warning, and then the compile crashes thanks to the noop
function.
Making it possible to log inside functions
The only problem we have with our current setup is that we cannot throw an error from within a function since we built a mixin. A mixin cannot be included inside a function (as it is likely to print CSS code, which has nothing to do in a Sass function)!
What if we transformed our mixin into a function for starters? At this point, there is something weird happening: @error
is not recognized as a valid at-directive for a function in Sass 3.3-, which thus fails miserably with:
Functions can only contain variable declarations and control directives.
Fair enough. It means we no longer need the noop
hack since unsupported engines crash without us having to force it. Although we have to put the @warn
directive upper in the flow so that the message gets printed in the author’s console before crashing.
@function log($message) {
@if feature-exists('at-error') != true {
@warn $message;
} @else {
@error $message;
}
}
Then, we can provide a mixin to have a more friendly API than dirty empty conditions and dummy variables hacks.
@mixin log($message) {
// Because functions cannot be called anywhere in Sass,
// we need to hack the call in a dummy condition.
// There are other ways to do this, such as `log: log(..)`.
@if log($message) {}
}
Final thoughts
That’s it! We can now use the log(..)
function inside functions (because of restrictions), and the log(..)
mixin anywhere else to responsibly throw an error. Pretty neat!
Here is the full code:
Play with this gist on SassMeister.
For a more advanced logging system in Sass, may I recommend you read Building A Logger Mixin. Regarding supporting old Sass versions, I suggest you have a look at When and How to Support Multiple Versions of Sass.