Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

An Introduction to Error Handling in Sass

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

No matter what the language, errors should be handled in a clean and simple way, whether that be for the benefit of the end user or the next developer. When you perform an incorrect action on a site, it shouldn't just punch you with an error code or a failing command. It should warn you with an explicative message about what's going on.

This shouldn't be any different when coding for other developers. When you build a library, a framework or simply work in a team, you're not the only one to use the code. Quite possibly, many developers with very different backgrounds may have to use your code, and they shouldn't need to dig deep into the source and the twists and turns of your mind to figure out what's going on.

This is why you should handle errors properly. Even in Sass. Even when it compiles to something as permissive as CSS. Whenever you are building a custom function or a mixin, you should always make sure everything works as expected and when it's not the case, you should take a different path. 

I don't know for you, but I really don't like letting the Sass compiler fail. I'd rather have a cool little message explaining to me that I've forgotten an argument rather than facing:

Long story short: handle errors correctly. I'll show you how.

What to Check?

Making Sure All Arguments Are Specified

One of the first things to do when building a function is making sure all arguments are specified. Unfortunately Sass won't even enter the function if the number of arguments passed to it is incorrect. Fortunately, the compiler handles it in a pretty way with a message like: Mixin my-awesome-mixin is missing argument $the-awesome-argument. so it's not that bad.

If you don't want to leave this in the hands of the compiler and would rather deal with it on your own, you can work around it by specifying default parameters to all arguments from the function/mixin. Not only is it better for the end user not to have to specify all arguments every single time, but it also allows you to trigger the mixin/function in all cases, and do whatever needs to be done.

Making Sure Arguments Are Valid

The most important thing now is to make sure all arguments are valid. For instance, if you need a color you have to make sure the developer is actually passing a color to the function and not a number or a string. If you need a list of three values, you should check the length to make sure there are three values, not two or four. 

Thankfully, Sass makes it very easy to do this with a bunch of default functions, including type-of() to check the data type of a value. By the way, Sass 3.4 should even introduce is-type-of($value, $type) to work around the issue of testing type of () which always returns list while it can be a map as well.

Anyway, even when writing a piece of code for developers with a decent knowledge of Sass you should pay attention to the input of your functions. 

For instance, when doing $value: $input + "px", you are basically casting a number (if $input is a number, obviously) to a string. Then if you try to do $value * 2, Sass will throw an error because you are trying to multiply a string. More information about this phenomenum in this article.

... in a function

Dealing with errors in case of a custom function is actually quite easy: you check for whatever needs to be checked. If the condition doesn't match, you return false (or null, see below) and warn the user with help from the @warn Sass directive.

@function add-10($number) {
  @if type-of($number) != "number" {
    @warn "`#{$number}` is not a number of `add-10`.";
    @return false;
  }

  @return $number + 10;
}

In this (quite silly I agree) case, if the $number argument is not a number, the function will return false and the user will be warned that they have passed a wrong argument by a message appearing in their terminal: `SWAG` is not a number for `add-10`.

If the argument is valid, then it simply returns the number + 10. Simple as pie.

... in a mixin

When dealing with a mixin, it's slightly more complicated because you don't actually return something; instead you dump CSS rules. To work around the problem, you need to declare a boolean that serves as a validity checker. Please consider the following example:

@mixin module($name) {
  // Initialize a validity checker boolean
  $everything-okay: true;

  // Check for argument validity
  @if type-of($name) != "string" {
    @warn "`#{$name}` is not a string for `module`.";
    $everything-okay: false;
  }

  // If everything's still okay, dump mixin content
  @if $everything-okay {
    @content;
  }
}

A Word About List

When you want to make sure one of the arguments is a list with multiple items in it, you should always test the length rather than the data type. To put it simply, Sass treats all values as single element lists, and in some cases you can find yourself with a single-item list which is actually still a list.

On the other hand, one potential issue when checking the length is that it can be a map as well. One way to make absolutely sure you're facing a list of multiple values is to do it like this:

@if length($value) > 1 and type-of($value) != map {
  // It is a list of multiple values
}

When is Checking Too Much Checking?

If I'm completely honest with you, I'd say never. You can never be too careful and could probably check for any argument of any function and any mixin of your application. However that's probably not necessary. I have a golden rule when it comes to argument checking: if the value is likely to make the Sass compiler crash, then you should have checked it

That means if you have a mixin accepting arguments that are directly used as values for CSS properties, I don't think there's any point in checking them aggressively. The worst case scenario is that those values will be dumped in the CSS output, resulting in invalid CSS which won't be understood by the browser. Apart from a couple of extra bytes loaded, there won't be much harm.

For instance, consider the following mixin:

@mixin size($width, $height: $width) {
  width: $width;
  height: $height;
}

And now consider the following usage:

.element {
  @include size(10, "string");
}

The resulting CSS will be:

.element {
  width: 10;
  height: "string";
}

Any browser will simply reject both properties because their values are invalid. No big deal.

Handling Errors in Deeply Nested Functions

With what we've just seen, you should be able to correctly handle 99% of possible errors, however there is a case where it's getting harder to keep up with Sass: when dealing with errors from a function called from a function called from a function...

I faced this very thing when writing SassyJSON, a JSON parser written in Sass. Basically you pass a JSON string to the json-decode function, which relays it to the  _json-decode--value function, which then relays it to a _json-decode--* function depending on the type of the value that's being parsed.  Anyway, it wasn't uncommon for me to have three, four, maybe five function calls nested within each other.

Unfortunately, in such a case, there is no clean way to stop the execution. You have to make your errors bubble up to the upper level by manually checking the function return at every level. For instance, if you have an error in the deepest function, it should return false to the calling function, which should then check this value, realize it's false, so return false to the upper function, and so on until you reach the highest one which then stops the execution.

That's quite a pain to deal with but there is no choice. That being said, if you have to nest several levels of functions, I'd say you're not using Sass correctly.

What to Return in Case of an Error

Whenever something goes wrong, you can return various values, false and null being the most popular ones. I suppose you could return -1 or void 0 if you come from a JavaScript background. I guess you could also return something like ERROR::{{ CODE }} although that would make the check for a failure slightly more complicated.

I use to return false because that speaks for itself: it's false. However this might be an issue with functions which are supposed to return a boolean. How would you know if the function returns false because the result has been evaluated to false, or false because there has been an error?

Thus, you can decide to pick another value. There is one interesting thing with null: a CSS property with a value of null won't be compiled to the source. Basically that means that if you use a Sass function as a value for a CSS property and something's wrong, the CSS property will simply be omitted by the Sass compiler.

I think that's actually kind of nice for something like this:

.element {
  background: get-color-from-theme('admin');
}

If the function get-color-from-theme has been well designed, it should make sure the admin theme exists and is actually bound to a color value. If admin doesn't exist, then the function would return null which would result in background: null. This will make Sass omit the rule when compiling the file to CSS.

In Need of a throw() Function

Most languages have a throw function or directive. Usually, this function accepts an error message or an exception as only argument and simply stops the script execution to display this message (in the console or something) whenever there is an error. For instance in JavaScript, you can do something like this:

if (typeof myVariable === "undefined") {
  throw new userException("`myVariable` should be defined");
} 

Unfortunately, Sass doesn't have this. Basically there is absolutely no way in Sass to stop a function or a mixin from executing. You could build one but it wouldn't practically stop the function, it would just return false and log a message.

@function throw($log) {
  @warn $log;
  @return false;
}

Instead of manually writing the @warn directive and the @return false every time, you can use the throw() function however remember it doesn't do any magic nor does it stop the script from running. It simply returns `false` and logs an error message.

That being said, having a throw directive has been asked quite a few times to Sass maintainers and it seems they've agreed it wouldn't be so bad to implement an @error directive that would break the current scope execution according to issue #747. Perhaps for 3.4, I'm not sure.

Final Thoughts

No matter how you do it, you should always check function and mixin inputs rather than let the compiler crash because it's encountered something unexpected. As a developer, I'd rather have a clean log and nothing output rather than a huge stack trace, but again that's not me.

As a live example, I suggest you have a look at a small button library I built some time back. Most of the code from the core mixin is argument checking and error handling. It makes the mixin look crowded, but I think it's essential for developers who will be using the code.

In the end, just make sure you come up with something that suits both you and your team and you'll be fine. 

Advertisement