Error Boundaries

Catch rendering errors in templates with try/fallback blocks

3 min read 694 words

{% try %}...{% fallback %}...{% end %}catches rendering errors inside a template and renders fallback content instead of crashing the page.

Basic usage

{% try %}
  {{ user.profile.avatar_url }}
{% fallback %}
  <img src="/default-avatar.png">
{% end %}

If user.profile.avatar_url raises an UndefinedError, the fallback renders. If no error occurs, the body renders normally and the fallback is ignored.

What gets caught

Error boundaries catch these exception types:

Exception Example
UndefinedError {{ missing_var }}
TemplateRuntimeError Errors from includes, macros, filters
TypeError {{ 42 \| join(",") }}(filter type mismatch)
ValueError Value conversion failures

Syntax errors are not caught -- they are raised at parse time, before rendering begins.

Partial output is discarded

When the body errors partway through, any output already produced by that body is thrown away. Only the fallback renders:

{% try %}
  <p>Hello, {{ user.name }}!</p>
  <p>Your role: {{ user.role }}</p>    {# errors here #}
{% fallback %}
  <p>Could not load user info.</p>
{% end %}

Even though Hello, Alice!was rendered before the error, the entire body is discarded and onlyCould not load user info.appears.

Accessing the error

Add a name after{% fallback %}to bind the caught error as a dict:

{% try %}
  {{ widget.render() }}
{% fallback err %}
  <div class="error">
    <p>Widget failed: {{ err.message }}</p>
    <small>{{ err.type }}</small>
  </div>
{% end %}

The error dict has these fields:

Field Type Description
message str The exception message
type str Exception class name (e.g."UndefinedError")
template str | None Template name where the error occurred
line int | None Line number of the error

Nesting

Error boundaries nest. The innermost{% try %}catches first:

{% try %}
  {% try %}
    {{ risky_operation() }}
  {% fallback %}
    <p>Inner fallback</p>
  {% end %}
{% fallback %}
  <p>Outer fallback (only if inner also fails)</p>
{% end %}

If the inner fallback itself errors, the outer boundary catches it.

Inside loops

Use{% try %}inside a loop to handle errors per-iteration without breaking the whole list:

<ul>
{% for item in items %}
  <li>
    {% try %}
      {{ render_item(item) }}
    {% fallback %}
      <span class="error">Failed to render item</span>
    {% end %}
  </li>
{% end %}
</ul>

One bad item doesn't take down the rest of the list.

With components

Error boundaries catch errors from any template code -- includes, macros, and component calls:

{# Catch errors from an included partial #}
{% try %}
  {% include "widgets/dashboard.html" %}
{% fallback %}
  <p>Dashboard unavailable.</p>
{% end %}

{# Catch errors from a component #}
{% try %}
  {{ chart(data=analytics_data) }}
{% fallback err %}
  <p>Chart error: {{ err.message }}</p>
{% end %}

Streaming

Error boundaries work correctly in streaming mode (render_stream()). The try body is buffered internally -- chunks are only yielded to the stream after the body completes without error. If the body errors, the buffer is discarded and the fallback streams normally.

for chunk in template.render_stream():
    response.write(chunk)

No partial try-body output leaks into the stream.

Patterns

Graceful degradation

Wrap optional sections so the page still renders when a data source fails:

<main>
  <h1>{{ page.title }}</h1>
  {{ page.content }}

  {% try %}
    {% include "partials/recommendations.html" %}
  {% fallback %}
    {# Recommendations unavailable -- page still works #}
  {% end %}
</main>

Development vs. production

Show error details in development, hide them in production:

{% try %}
  {{ complex_widget() }}
{% fallback err %}
  {% if debug %}
    <pre>{{ err.type }}: {{ err.message }}</pre>
  {% else %}
    <p>Something went wrong.</p>
  {% end %}
{% end %}

Per-item resilience in data-driven pages

{% for post in posts %}
  {% try %}
    <article>
      <h2>{{ post.title }}</h2>
      <p>{{ post.excerpt }}</p>
      <span>By {{ post.author.name }}</span>
    </article>
  {% fallback %}
    <article class="error">
      <h2>Post unavailable</h2>
    </article>
  {% end %}
{% end %}