Deep Dive

Logging and Error Handling in Oorian: A Complete Guide

Oorian provides a lightweight logging facade and a layered error handling system that give you full control without imposing a specific logging framework or error strategy. Here's how they work.

The Oorian TeamFebruary 24, 20266 min read
Logging and Error Handling in Oorian: A Complete Guide

Production applications need two things to stay healthy: clear logging and reliable error handling. Oorian provides both out of the box, with a lightweight logging facade and a layered error handling system that are entirely opt-in and framework-agnostic. In this post, we'll walk through how both systems work and how to get the most out of them.

Logging with OorianLog

Rather than bundling a specific logging framework, Oorian delegates to the JDK's System.Logger platform logging SPI, introduced in Java 9. This means your application can use any logging backend—SLF4J, Log4j2, java.util.logging, or any other provider—without Oorian requiring additional dependencies.

Getting Started

Create a logger instance as a private static final field in each class that needs logging:

public class OrderService
{
    private static final OorianLog LOG = OorianLog.getLogger(OrderService.class);

    public void processOrder(String orderId)
    {
        LOG.info("Processing order {0}", orderId);

        try
        {
            // ... process the order
            LOG.debug("Order {0} completed successfully", orderId);
        }
        catch (Exception ex)
        {
            LOG.error("Failed to process order", ex);
        }
    }
}

Each logger is named after the fully qualified class name, so log output always identifies its source. The logger instance is lightweight and safe to store as a static field.

Five Log Levels

OorianLog provides five log levels, each mapped to a System.Logger.Level:

  • TRACE — Fine-grained detail: per-message operations, heartbeats, pings
  • DEBUG — Diagnostic information: request routing, connections opening and closing, session lifecycle
  • INFO — Lifecycle events: application startup and shutdown, license status, registration counts
  • WARNING — Recoverable issues: CSRF validation failures, missing resources, security events
  • ERROR — Failures: exceptions that affect functionality, unrecoverable errors

A good rule of thumb: reserve INFO for true lifecycle events. A healthy application running in steady state should produce little or no INFO output. Use DEBUG for request-level detail and TRACE for per-message granularity.

Three Method Overloads Per Level

Each level provides three overloads for different use cases:

// Simple message
LOG.info("Application started");

// Message with exception (stack trace is included in the output)
LOG.error("Failed to load configuration", ex);

// Parameterized message using MessageFormat syntax: {0}, {1}, ...
LOG.debug("Processing page {0} for session {1}", pageId, sessionId);

Parameterized messages use java.text.MessageFormat syntax with numbered placeholders. This is more efficient than string concatenation because the message is only formatted if the level is enabled.

Guarding Expensive Construction

If building a log message is expensive (e.g., serializing objects or iterating over large collections), use isLoggable() to check whether the level is enabled before constructing the message:

if (LOG.isLoggable(System.Logger.Level.DEBUG))
{
    String details = buildExpensiveDebugInfo(request);
    LOG.debug("Request details: {0}", details);
}

For simple parameterized messages, this guard is not necessary—the framework avoids formatting the message if the level is disabled.

Pluggable Backends

By default, System.Logger delegates to java.util.logging (JUL). To route Oorian log output through a different logging framework, add the appropriate bridge library to your classpath:

  • SLF4J (Logback, etc.)org.slf4j:slf4j-jdk-platform-logging
  • Log4j2org.apache.logging.log4j:log4j-jpl
  • JUL — No additional dependency required (default)

Once a bridge is on the classpath, all Oorian log output is automatically routed through your chosen backend. Oorian does not require any logging configuration to run—add a bridge library only if you want to unify Oorian's logging with your application's existing logging framework.

Error Handling

Oorian provides a layered error handling system that covers custom error pages, built-in defaults, centralized exception monitoring, and development-mode diagnostics. All error handling features are opt-in and configured in your Application.initialize() method.

Custom Error Pages

Custom error pages extend ErrorPage and implement createBody(Body body). Build your page using standard Oorian elements—the base class handles the HTML document skeleton, charset, viewport, and a minimal CSS reset:

public class NotFoundPage extends ErrorPage
{
    @Override
    protected void createBody(Body body)
    {
        body.addElement(new H1("Error " + getStatusCode()));
        body.addElement(new Paragraph(getMessage()));
        body.addElement(new Paragraph("Path: " + getRequestPath()));
    }
}

The ErrorPage base class provides convenience getters: getStatusCode(), getRequestPath(), getException(), getTitle(), and getMessage(). Error pages extend ErrorPage, not HtmlPage—they are lightweight, standalone HTML pages that avoid the WebSocket/event overhead and render reliably even when the normal page infrastructure has failed.

Registering Error Pages

Register custom error pages for specific HTTP status codes in your Application class:

@Override
protected void initialize(ServletContext context)
{
    registerPackage("com.myapp");

    // Register custom error pages for specific status codes
    setErrorPage(404, NotFoundPage.class);
    setErrorPage(403, ForbiddenPage.class);
    setErrorPage(500, ServerErrorPage.class);

    // Set a default for any status code without a specific page
    setDefaultErrorPage(GenericErrorPage.class);
}

The lookup order is: status-specific page → default error page → built-in default. If no custom error pages are registered, Oorian's built-in DefaultErrorPage automatically activates as the fallback for all status codes, rendering a clean, user-friendly message with the status code, description, and a “Return to Home” link.

Centralized Exception Handler

For application-wide exception monitoring, logging, and alerting, implement the ExceptionHandler interface:

public class AppExceptionHandler implements ExceptionHandler
{
    private static final OorianLog LOG =
        OorianLog.getLogger(AppExceptionHandler.class);

    @Override
    public void handle(Exception exception, HtmlPage page)
    {
        LOG.error("Unhandled exception", exception);
        alertService.notify(exception);
    }
}

Register it in your Application:

setExceptionHandler(new AppExceptionHandler());

The handler integrates at three points in the framework:

  • Page creation — exceptions during initializePage(), createHead(), or createBody()
  • WebSocket message handling — exceptions while processing client events
  • Worker threads — exceptions in OorianWorkerThread execution

The centralized handler is invoked before the page-level onException() hook, giving you a single place to add logging or alerting without modifying individual pages.

Development Mode

In production (the default), error pages show a user-friendly message with no technical details. In development mode, they include the request path, exception class, message, and full stack trace.

Enable it programmatically:

setDevMode(true);

Or via a system property (recommended):

-Doorian.mode=dev

The system property approach is preferred because a setDevMode(true) call in initialize() can easily be overlooked during code review and accidentally deployed to production. Never enable development mode in production—it exposes stack traces, class names, and request paths that could help an attacker understand your application's internals.

Putting It All Together

Here's what a well-configured application looks like with both logging and error handling set up:

@WebListener
public class MyApp extends Application
{
    @Override
    protected void initialize(ServletContext context)
    {
        registerPackage("com.myapp");

        // Custom error pages
        setErrorPage(404, NotFoundPage.class);
        setErrorPage(500, ServerErrorPage.class);
        setDefaultErrorPage(GenericErrorPage.class);

        // Centralized exception monitoring
        setExceptionHandler(new AppExceptionHandler());
    }
}

With this configuration, your application has clean logging through whatever backend you prefer, branded error pages for your users, and centralized exception monitoring for your operations team—all without a single line of JavaScript.

Share this article

Related Articles

Deep Dive

Oorian Add-Ons: Server-Side Building Blocks for Real Applications

February 17, 2026
Deep Dive

Oorian's Built-In JavaScript APIs: Control the Browser from Java

February 12, 2026
Deep Dive

CSS Styling in Oorian

February 10, 2026