Apache FreeMarker is a template engine: a Java library to generate text output based on templates and changing data.
—
FreeMarker provides a comprehensive exception handling system that helps identify and manage errors during template processing, parsing, and model operations.
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);
}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);
}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();
}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();
}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();
}These exceptions are used internally for template control flow:
class StopException extends TemplateException {
StopException(Environment env);
StopException(String description, Environment env);
}class ReturnException extends TemplateException {
ReturnException(TemplateModel returnValue, Environment env);
TemplateModel getReturnValue();
}class BreakException extends TemplateException {
BreakException(Environment env);
}
class ContinueException extends TemplateException {
ContinueException(Environment env);
}interface TemplateExceptionHandler {
void handleTemplateException(TemplateException te, Environment env, Writer out)
throws TemplateException;
}// 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;Silently ignores exceptions and continues processing:
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER);
// Exceptions are logged but don't interrupt template processingPrints exception information to the output:
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);
// Output includes: [ERROR: expression_that_failed]Prints HTML-escaped exception information:
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
// Safe for HTML output: <ERROR: expression_that_failed>Re-throws exceptions for proper error handling:
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
// Exceptions are re-thrown to be handled by application codepublic 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());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;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());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);
}
}
}<#-- 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>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;
}
}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();
}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);
}
}
}
}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 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
)
);@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