Skip to content

The default filter should only evaluate the expression passed to it if necessary #4668

Open

Description

@MoritzLost

Not sure if this is a bug or just unexpected behavior, but I just found that the expression passed to the default filter will always evaluate the expression that is passed to it, even when it's not required.

{% set foo = bar | default(some_expensive_function())%}

In this example, from a template developer's perspective, I would expect that some_expensive_function() is only called if bar is falsy. However, it's always called, no matter the value of bar.

This is a problem when the parameter is expensive to compute or requires a database call. For example, in Craft CMS template, I often do something like this:

{% set image = image | default(entry.image.one()) %}

This will either take the image passed to the template, or load a fallback image from the database. I only want to run the additional database query when image is empty.

I can use null coalescing, which behaves differently. But sometimes I want the default filter, to also handle cases where the first variable may be falsy, but not null.

I don't know how complicated this change would be. At least in theory, I don't see any reason why the default filter couldn't use short-circuiting in its generated PHP code to avoid running the expression unnecessarily.

Activity

  1. gerryvdm commented on Jan 8, 2026

    @gerryvdm

    This looks like expected behaviour that the filter arguments are eagerly evaluated. Filters are not control structures.

    You could probably accomplish this is another way, like with a ternary condition:

    set image = image is defined and image is not empty ? image : entry.image.one()

  2. jannes-io commented on Jan 8, 2026

    @jannes-io

    While it may be "intended" that the expression is evaluated, I personally also never thought of it, and been putting values/expressions into the |default filter as if it was a replacement for ?? or defined/empty tests with little regard to what's actually going in there.

    I really think handling of null, empty or other falsly values could be vastly improved for Twig. If not through |default, then perhaps something new. Once the dev gets to the template, they shouldn't have to really think about the types anymore, and a very simple, concise, in-line way to say "falsly or this value" would definitely be appreciated.

    You could probably accomplish this is another way, like with a ternary condition:

    In this case the example is quite easy, but imagine if you need to test several levels deep, where each level could be null or even "undefined":

    {{  foo.bar.baz|default(expensive_expr()) }}

    Would become

    {% set thing = foo is defined and foo is not empty
        ? (foo.bar is defined and foo.bar is not empty
            ? (foo.bar.baz is defined and foo.bar.baz is not empty
                ? foo.bar.baz : expensive_expr()) : expensive_expr()) : expensive_expr() %}

    Perhaps we should allow passing a callable into |default that is executed, so we can write something like:

    {% set thing = foo.bar.baz|default(() => expensive_expr()) %}

    Although this would be a BC break, since passing a callable into |default is something you can already do now, and rn you'd expect the callable to just be returned, not executed..

    A non BC solution would be a special parser that takes |default, and wraps it in a function that only gets called if the lhs is undefined/null/falsly.. Or a new filter/control structure altogether?

  3. stof commented on Sep 25, 2026

    @stof
    Member

    truthy or this value is covered by the ?: operator, which short-circuits properly. non-null or this value is covered by the ?? operator (which handles undefined values gracefully), which also short-circuits.
    In practice, I almost never use the default filter anymore (which is unsafe to use on boolean values, as foo.isMatching|default(true) will actually never be false as the empty test considers false as empty)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions