If you want a thumbnail generating filter in Nunjucks you'll probably run into some problems. We'll run through an async filter example. The filter will handle optional arguments and we'll look at common problems.

Async functionality and Nunjucks don't work well together. I ran into quite some issues trying to create a thumbnail filter. We'll go through a detailed asynchronous filter here to complement the Nunjucks documentation.

Example

The example we'll go through here is a bit contrived, but should clarify the use of optional parameters. At the end we'll have a filter that will render the following templates:

Template: Hello {{ "world" | asyncFilter }}
Result: Hello world wide

Template: Hello {{ "world" | asyncFilter("stage") }}
Result: Hello world stage

Full example code is available on codepen.

Filter definition

Nunjucks async filters are defined like normal filters with true as the third parameter. The last parameter passed to your filter is callback, a function that should be called when your filter is done.

Note that you should call callback(null, result) on success! The first argument is for the error callback callback(myError)...!

We'll do a little trick to unpack the arguments so we can pass optional arguments. First the last argument (callback) is popped from the args variable. Secondly we'll check if the second argument is set (our optional argument).

env.addFilter('asyncFilter', (...args) => {
    const callback = args.pop();
    const message = args[0];
    const extraMessage = args.length < 2 ? 'wide' : args[1]
  
    setTimeout(() => {
      callback(null, `${message} ${extraMessage}`);
    }, 100);
}, true);

Rendering

Nunjucks must be forced into asynchronous rendering for our filter to work. Passing the callback to the render functions will make this happen:

env.renderString(template, context, (error, result) => {
    if (error) {
      console.error('template render failed', error);
    } else {
      console.log(result);
      // Use results
    }
});

Usage

You can use these filters just like any normal filter.

Using default value:

Hello {{ "world" | asyncFilter }}

With argument set:

Hello {{ "world" | asyncFilter("stage") }}

Issues

Using async filters, especially using optional arguments, isn't straight forward. There a quite some caveats. We've already seen you should call callback(null, result) in your filter, don't forget!

Also make sure you are using the asynchronous renderer.

And then there are macros usage...

Macros don't mix with asynchronous filters!!!

If you use a asynchronous filter in a macro, the filter is handled like a normal synchronous filter.

It's to bad. Because that leaves us without a tool that properly isolates our scope when doing template inclusion.

I think a normal include should work. Though I have not tested this. To get proper variable passing, you might try this include hack.

Async loops

The documentation mentions async versions of for each loops. I haven't ran into an issue so far, but I suggest you use asyncEach and asyncAll when you start using async filters.