CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-freemarker--freemarker

Apache FreeMarker is a template engine: a Java library to generate text output based on templates and changing data.

Pending
Overview
Eval results
Files

exception-handling.mddocs/

Exception Handling

FreeMarker provides a comprehensive exception handling system that helps identify and manage errors during template processing, parsing, and model operations.

Core Exception Classes

Base Template Exception

class TemplateException extends Exception {
  // Constructors
  TemplateException(String description, Environment env);
  TemplateException(String description, Environment env, Throwable cause);
  TemplateException(Throwable cause, Environment env);
  
  // Template context information
  String getFTLInstructionStack();
  int getLineNumber();  
  int getColumnNumber();
  String getTemplateName();
  Environment getEnvironment();
  
  // Exception details
  String getDescription();
  Throwable getCause();
  String getBlamedExpressionString();
  String getMessageWithoutStackTop();
  
  // Stack trace utilities
  void printStackTrace(PrintWriter pw);
  void printStackTrace(PrintStream ps);
}

Template Model Exception

Exception for template model operations:

class TemplateModelException extends TemplateException {
  // Constructors
  TemplateModelException(String description);
  TemplateModelException(String description, Throwable cause);
  TemplateModelException(Throwable cause);
  
  // Create with environment context when available
  TemplateModelException(String description, Environment env);
  TemplateModelException(String description, Environment env, Throwable cause);
}

Template Not Found Exception

Exception when templates cannot be located:

class TemplateNotFoundException extends IOException {
  // Constructors
  TemplateNotFoundException(String templateName, Object customLookupCondition, String reason);
  TemplateNotFoundException(String templateName, Object customLookupCondition, String reason, Throwable cause);
  
  // Template information
  String getTemplateName();
  Object getCustomLookupCondition();
}

Malformed Template Name Exception

Exception for invalid template names:

class MalformedTemplateNameException extends IOException {
  // Constructors
  MalformedTemplateNameException(String templateName, String reason);
  MalformedTemplateNameException(String templateName, String reason, Throwable cause);
  
  // Template name information  
  String getTemplateName();
}

Parsing Exceptions

Parse Exception

Exception during template parsing:

class ParseException extends IOException {
  // Constructors (inherited from JavaCC ParseException)
  ParseException(String message);
  ParseException(Token currentTokenVal, int[][] expectedTokenSequences, String[] tokenImage);
  ParseException();
  
  // Parser context information
  Token currentToken;
  int[][] expectedTokenSequences;
  String[] tokenImage;
  
  // Position information
  int getLineNumber();
  int getColumnNumber();
  String getTemplateName();
}

Control Flow Exceptions

These exceptions are used internally for template control flow:

Stop Exception

class StopException extends TemplateException {
  StopException(Environment env);
  StopException(String description, Environment env);
}

Return Exception

class ReturnException extends TemplateException {
  ReturnException(TemplateModel returnValue, Environment env);
  TemplateModel getReturnValue();
}

Break and Continue Exceptions

class BreakException extends TemplateException {
  BreakException(Environment env);
}

class ContinueException extends TemplateException {
  ContinueException(Environment env);
}

Exception Handlers

Template Exception Handler Interface

interface TemplateExceptionHandler {
  void handleTemplateException(TemplateException te, Environment env, Writer out) 
    throws TemplateException;
}

Built-in Exception Handlers

// Predefined exception handlers in TemplateExceptionHandler
static final TemplateExceptionHandler IGNORE_HANDLER = IgnoreTemplateExceptionHandler.INSTANCE;
static final TemplateExceptionHandler DEBUG_HANDLER = DebugTemplateExceptionHandler.INSTANCE;  
static final TemplateExceptionHandler HTML_DEBUG_HANDLER = HtmlDebugTemplateExceptionHandler.INSTANCE;
static final TemplateExceptionHandler RETHROW_HANDLER = RethrowTemplateExceptionHandler.INSTANCE;

Ignore Handler

Silently ignores exceptions and continues processing:

cfg.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER);
// Exceptions are logged but don't interrupt template processing

Debug Handler

Prints exception information to the output:

cfg.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);
// Output includes: [ERROR: expression_that_failed]

HTML Debug Handler

Prints HTML-escaped exception information:

cfg.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
// Safe for HTML output: <ERROR: expression_that_failed>

Rethrow Handler (Recommended)

Re-throws exceptions for proper error handling:

cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
// Exceptions are re-thrown to be handled by application code

Custom Exception Handlers

public class CustomExceptionHandler implements TemplateExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);
    
    @Override
    public void handleTemplateException(TemplateException te, Environment env, Writer out) 
        throws TemplateException {
        
        // Log the exception with context
        logger.error("Template processing error in template '{}' at line {}: {}", 
                    te.getTemplateName(), te.getLineNumber(), te.getMessage(), te);
        
        try {
            // Write user-friendly error message to output
            out.write("<!-- Error occurred. See logs for details. -->");
            
            // In development mode, include more details
            if (isDevelopmentMode()) {
                out.write("\n<!-- ");
                out.write("Template: " + te.getTemplateName());
                out.write(", Line: " + te.getLineNumber());
                out.write(", Error: " + te.getMessage());
                out.write(" -->");
            }
        } catch (IOException e) {
            throw new TemplateException("Failed to write error message", env, e);
        }
    }
    
    private boolean isDevelopmentMode() {
        // Implementation depends on your application
        return "development".equals(System.getProperty("app.mode"));
    }
}

// Usage
cfg.setTemplateExceptionHandler(new CustomExceptionHandler());

Attempt Exception Reporter

Handle exceptions from the #attempt directive:

interface AttemptExceptionReporter {
  void report(TemplateException te, Environment env);
}

// Built-in reporters
static final AttemptExceptionReporter LOG_ERROR_REPORTER = LoggingAttemptExceptionReporter.ERROR_REPORTER;
static final AttemptExceptionReporter LOG_WARN_REPORTER = LoggingAttemptExceptionReporter.WARN_REPORTER;

Custom Attempt Exception Reporter

public class CustomAttemptExceptionReporter implements AttemptExceptionReporter {
    private static final Logger logger = LoggerFactory.getLogger(CustomAttemptExceptionReporter.class);
    
    @Override
    public void report(TemplateException te, Environment env) {
        // Log attempt failures with context
        logger.warn("Attempt directive failed in template '{}' at line {}: {}", 
                   te.getTemplateName(), te.getLineNumber(), te.getMessage());
        
        // Could also send to error tracking service
        ErrorTracker.recordAttemptFailure(te.getTemplateName(), te.getMessage());
    }
}

// Configuration
cfg.setAttemptExceptionReporter(new CustomAttemptExceptionReporter());

Exception Handling Best Practices

Application-Level Exception Handling

public class TemplateProcessor {
    private final Configuration config;
    
    public String processTemplate(String templateName, Object dataModel) throws ProcessingException {
        try {
            Template template = config.getTemplate(templateName);
            StringWriter out = new StringWriter();
            template.process(dataModel, out);
            return out.toString();
            
        } catch (TemplateNotFoundException e) {
            throw new ProcessingException("Template not found: " + e.getTemplateName(), e);
            
        } catch (MalformedTemplateNameException e) {
            throw new ProcessingException("Invalid template name: " + e.getTemplateName(), e);
            
        } catch (ParseException e) {
            throw new ProcessingException(
                String.format("Template parsing failed at line %d, column %d: %s", 
                             e.getLineNumber(), e.getColumnNumber(), e.getMessage()), e);
                             
        } catch (TemplateException e) {
            throw new ProcessingException(
                String.format("Template processing failed in '%s' at line %d: %s",
                             e.getTemplateName(), e.getLineNumber(), e.getMessage()), e);
                             
        } catch (IOException e) {
            throw new ProcessingException("I/O error during template processing", e);
        }
    }
}

Template-Level Error Handling

<#-- Handle missing variables gracefully -->
<h1>${title!"Default Title"}</h1>

<#-- Use attempt directive for risky operations -->
<#attempt>
    <#include "optional-content.ftl">
<#recover>
    <p>Optional content not available.</p>
</#attempt>

<#-- Safe property access -->
<#if user??>
    Welcome, ${user.name!"Anonymous"}!
<#else>
    Please log in.
</#if>

<#-- Safe method calls -->
<#if utils?? && utils.formatDate??>
    Today: ${utils.formatDate(date, "yyyy-MM-dd")}
<#else>
    Today: ${date?string}
</#if>

Model Exception Handling

public class SafeUserModel implements TemplateHashModel {
    private final User user;
    
    public SafeUserModel(User user) {
        this.user = user;
    }
    
    @Override
    public TemplateModel get(String key) throws TemplateModelException {
        try {
            switch (key) {
                case "name":
                    return new SimpleScalar(user.getName() != null ? user.getName() : "");
                case "email":
                    return new SimpleScalar(user.getEmail() != null ? user.getEmail() : "");
                case "age":
                    return new SimpleNumber(user.getAge());
                case "fullName":
                    return new SimpleScalar(getFullName());
                default:
                    return null;
            }
        } catch (Exception e) {
            throw new TemplateModelException("Error accessing user property: " + key, e);
        }
    }
    
    private String getFullName() throws TemplateModelException {
        try {
            String firstName = user.getFirstName();
            String lastName = user.getLastName();
            
            if (firstName == null && lastName == null) {
                return "";
            } else if (firstName == null) {
                return lastName;
            } else if (lastName == null) {
                return firstName;  
            } else {
                return firstName + " " + lastName;
            }
        } catch (Exception e) {
            throw new TemplateModelException("Error building full name", e);
        }
    }
    
    @Override
    public boolean isEmpty() throws TemplateModelException {
        return user == null;
    }
}

Error Information and Debugging

Exception Context Information

try {
    template.process(dataModel, out);
} catch (TemplateException e) {
    // Get detailed error information
    System.err.println("Template Name: " + e.getTemplateName());
    System.err.println("Line Number: " + e.getLineNumber());
    System.err.println("Column Number: " + e.getColumnNumber());
    System.err.println("FTL Instruction Stack: " + e.getFTLInstructionStack());
    System.err.println("Blamed Expression: " + e.getBlamedExpressionString());
    
    // Print full context
    e.printStackTrace();
}

Development vs Production Error Handling

public class EnvironmentAwareExceptionHandler implements TemplateExceptionHandler {
    private final boolean isDevelopment;
    
    public EnvironmentAwareExceptionHandler(boolean isDevelopment) {
        this.isDevelopment = isDevelopment;
    }
    
    @Override
    public void handleTemplateException(TemplateException te, Environment env, Writer out) 
        throws TemplateException {
        
        if (isDevelopment) {
            // Development: Show detailed error information
            try {
                out.write("\n<!-- TEMPLATE ERROR -->\n");
                out.write("<!-- Template: " + te.getTemplateName() + " -->\n");
                out.write("<!-- Line: " + te.getLineNumber() + ", Column: " + te.getColumnNumber() + " -->\n");
                out.write("<!-- Error: " + te.getMessage() + " -->\n");
                out.write("<!-- FTL Stack: " + te.getFTLInstructionStack() + " -->\n");
                out.write("<!-- END TEMPLATE ERROR -->\n");
            } catch (IOException e) {
                throw new TemplateException("Failed to write debug information", env, e);
            }
        } else {
            // Production: Log error and show generic message
            logger.error("Template processing error", te);
            try {
                out.write("<!-- An error occurred while processing this section -->");
            } catch (IOException e) {
                throw new TemplateException("Failed to write error placeholder", env, e);
            }
        }
    }
}

Exception Monitoring and Alerting

public class MonitoringExceptionHandler implements TemplateExceptionHandler {
    private final TemplateExceptionHandler delegate;
    private final MetricRegistry metrics;
    private final Counter errorCounter;
    
    public MonitoringExceptionHandler(TemplateExceptionHandler delegate, MetricRegistry metrics) {
        this.delegate = delegate;
        this.metrics = metrics;
        this.errorCounter = metrics.counter("template.errors");
    }
    
    @Override
    public void handleTemplateException(TemplateException te, Environment env, Writer out) 
        throws TemplateException {
        
        // Record metrics
        errorCounter.inc();
        metrics.counter("template.errors.by-template", "template", te.getTemplateName()).inc();
        
        // Record error details for monitoring
        ErrorEvent event = new ErrorEvent()
            .setTemplateName(te.getTemplateName())
            .setLineNumber(te.getLineNumber())
            .setErrorMessage(te.getMessage())
            .setTimestamp(System.currentTimeMillis());
        
        // Send to monitoring system
        MonitoringService.recordError(event);
        
        // Alert on critical errors
        if (isCriticalError(te)) {
            AlertingService.sendAlert("Critical template error", te.getMessage());
        }
        
        // Delegate to original handler
        delegate.handleTemplateException(te, env, out);
    }
    
    private boolean isCriticalError(TemplateException te) {
        // Define criteria for critical errors
        return te.getTemplateName().startsWith("critical/") ||
               te.getMessage().contains("database") ||
               te.getMessage().contains("security");
    }
}

Configuration for Error Handling

Comprehensive Error Handling Setup

Configuration cfg = new Configuration(Configuration.VERSION_2_3_34);

// Basic error handling configuration
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
cfg.setAttemptExceptionReporter(AttemptExceptionReporter.LOG_WARN_REPORTER);

// Control exception logging
cfg.setLogTemplateExceptions(false);  // Handle logging in exception handler
cfg.setWrapUncheckedExceptions(true); // Wrap RuntimeExceptions in TemplateException

// Development vs Production configuration
if (isDevelopmentMode()) {
    cfg.setTemplateExceptionHandler(new DevelopmentExceptionHandler());
} else {
    cfg.setTemplateExceptionHandler(new ProductionExceptionHandler());
}

// Custom error handling for specific scenarios
cfg.setTemplateExceptionHandler(
    new ChainedExceptionHandler(
        new SecurityExceptionHandler(),     // Handle security-related errors first
        new DatabaseExceptionHandler(),    // Handle database errors
        new DefaultExceptionHandler()      // Handle everything else
    )
);

Testing Exception Handling

@Test
public void testTemplateExceptionHandling() {
    Configuration cfg = new Configuration(Configuration.VERSION_2_3_34);
    TestExceptionHandler handler = new TestExceptionHandler();
    cfg.setTemplateExceptionHandler(handler);
    
    // Test with template that has undefined variable
    StringTemplateLoader loader = new StringTemplateLoader();
    loader.putTemplate("test.ftl", "${undefinedVariable}");
    cfg.setTemplateLoader(loader);
    
    try {
        Template template = cfg.getTemplate("test.ftl");
        template.process(new HashMap<>(), new StringWriter());
        fail("Expected TemplateException");
    } catch (TemplateException e) {
        assertEquals("undefined variable: undefinedVariable", e.getMessage());
        assertTrue(handler.wasExceptionHandled());
    }
}

class TestExceptionHandler implements TemplateExceptionHandler {
    private boolean exceptionHandled = false;
    
    @Override
    public void handleTemplateException(TemplateException te, Environment env, Writer out) 
        throws TemplateException {
        exceptionHandled = true;
        throw te; // Re-throw for test verification
    }
    
    public boolean wasExceptionHandled() {
        return exceptionHandled;
    }
}

Install with Tessl CLI

npx tessl i tessl/maven-org-freemarker--freemarker

docs

caching-loading.md

core-processing.md

exception-handling.md

extensions.md

index.md

object-wrapping.md

output-formats.md

template-models.md

tile.json