How spdlog's Error Handler Mechanism Works and How to Customize It

spdlog uses a callback-based error handling system where exceptions during logging are caught and forwarded to a user-configurable handler function, which can be set globally via spdlog::set_error_handler() or per-logger via logger::set_error_handler().

The spdlog library provides a robust, extensible error handling mechanism that prevents logging failures from crashing your application while giving you full control over how errors are reported. This article explains how the mechanism works internally in the gabime/spdlog repository and demonstrates how to implement custom handlers for production services, libraries, and debugging scenarios.

How the Error Handler Mechanism Works

spdlog protects every logging call with a try/catch wrapper inside logger::log(). When an internal operation fails—whether due to a sink write failure, formatting exception, or other error—the logger catches the exception and forwards a descriptive message to a handler function.

The mechanism supports two levels of handler configuration:

  • Global handler – set once for the entire process via spdlog::set_error_handler
  • Per-logger handler – each spdlog::logger instance can have its own handler via logger::set_error_handler

Both handlers are stored in spdlog::details::registry. The default global handler simply writes error messages to stderr, allowing the application to continue running.

Where the Code Lives

File Responsibility
include/spdlog/spdlog.h Declares spdlog::set_error_handler() global API at lines 90-92
include/spdlog/spdlog-inl.h Forwards global calls to the registry at lines 54-56
include/spdlog/logger.h Declares per-logger set_error_handler() at lines 310-311
include/spdlog/logger-inl.h Stores handler in logger instance at lines 112-114
include/spdlog/details/registry.h Holds global handler interface at lines 79-82
include/spdlog/details/registry-inl.h Propagates handler to new loggers at lines 165-168

Error Handling Flow

  1. Initialization – The registry creates a default handler writing to stderr
  2. Logging call – logger::log() wraps core work in try/catch
  3. Exception caught – Any exception builds a descriptive msg string
  4. Handler invoked – Per-logger handler takes priority; otherwise global handler runs
  5. User code executes – Your handler decides: log elsewhere, abort, throw, or ignore

The handler signature is intentionally simple:

void handler(const std::string &msg);

This accepts any callable: free functions, lambdas, functors, or std::function wrappers.

Setting a Global Error Handler

Use spdlog::set_error_handler() to establish one handler for all loggers in your process. This is the recommended approach for applications with uniform error handling needs.

#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>

int main() {
    // Redirect all spdlog internal errors to a dedicated error logger
    spdlog::set_error_handler([](const std::string &msg) {
        auto err_log = spdlog::stderr_color_mt("error_logger");
        err_log->error("*** SPDLOG INTERNAL ERROR ***: {}", msg);
        
        // Production option: abort on any logging failure
        // std::abort();
    });

    auto logger = spdlog::stdout_color_mt("app");
    logger->info("Normal logging proceeds");
    // Any internal failure now triggers the lambda above
}

According to the spdlog source code, the global handler is stored in the registry and automatically attached to all newly created loggers.

Setting a Per-Logger Error Handler

Use logger::set_error_handler() when specific loggers need distinct error behavior. This overrides the global handler for that logger only.

#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <stdexcept>

// Custom exception type for logging failures
struct logging_error : std::runtime_error { 
    using std::runtime_error::runtime_error; 
};

int main() {
    auto file_logger = spdlog::basic_logger_mt("file", "app.log");

    // This logger throws on internal errors
    file_logger->set_error_handler([](const std::string &msg) {
        throw logging_error("Logging failed: " + msg);
    });

    try {
        file_logger->info("This works normally");
        // Simulate failure condition (e.g., disk full, permissions error)
        // The handler above converts spdlog's internal error to your exception
    }
    catch (const logging_error &e) {
        std::cerr << "Caught custom error: " << e.what() << std::endl;
        // Application decides: retry, degrade, or terminate
    }
}

Important: When a logger has its own handler, the global handler is not invoked. This isolation lets you mix generic fallback behavior with specialized handling for critical paths.

Handler Priority and Interaction

The spdlog error handler mechanism follows a simple priority rule implemented in logger-inl.h:

  1. Check if logger has custom handler
  2. If yes, call it
  3. If no, call global handler from registry

This design supports flexible configurations:

Configuration Behavior
Global handler only All loggers use the same error handling
Per-logger handler only Specific logger uses custom handling; others use default stderr
Both set Logger with custom handler uses it; others use global
Neither set All loggers write errors to stderr via default handler

Common Custom Handler Patterns

Production Service: Abort on Failure

For services where logging integrity is critical, install a global handler that terminates the process after capturing diagnostic information:

spdlog::set_error_handler([](const std::string &msg) {
    // Write to emergency sink that's guaranteed to work (e.g., syslog)
    syslog(LOG_EMERG, "spdlog fatal error: %s", msg.c_str());
    
    // Prevent continued operation with corrupted log state
    std::abort();
});

Library Development: Surface Errors to Users

Libraries should use per-logger handlers to throw exceptions that calling code can catch and handle appropriately:

class MyLibrary {
    std::shared_ptr<spdlog::logger> log_;
public:
    MyLibrary() {
        log_ = spdlog::stdout_color_mt("mylib");
        log_->set_error_handler([](const std::string &msg) {
            throw LibraryException("Internal logging error: " + msg);
        });
    }
};

Debug Builds: Capture Full Diagnostics

#ifdef DEBUG
spdlog::set_error_handler([](const std::string &msg) {
    std::cerr << "SPDLOG ERROR: " << msg << std::endl;
    spdlog::dump_backtrace();  // Requires SPDLOG_ENABLE_BACKTRACE
    // Continue execution to allow debugging
});
#endif

Multi-Process Safe: Dedicated Error File

std::mutex error_mutex;
auto error_file = std::make_shared<spdlog::sinks::basic_file_sink_mt>("errors.log", true);

spdlog::set_error_handler([&](const std::string &msg) {
    std::lock_guard<std::mutex> lock(error_mutex);
    error_file->log(spdlog::details::log_msg{
        spdlog::source_loc{}, 
        "spdlog", 
        spdlog::level::err, 
        msg
    });
});

Thread Safety Considerations

The error handler mechanism is thread-safe. The registry uses internal synchronization when setting or retrieving the global handler. Per-logger handlers are stored as std::function members and accessed under the logger's mutex during log operations.

However, your handler implementation must be thread-safe if multiple threads may trigger logging errors concurrently. Use locks, atomic operations, or thread-safe sinks as appropriate.

Summary

  • spdlog's error handler mechanism is a callback-based system that catches exceptions during logging and forwards them to user-configurable handlers
  • Set handlers globally with spdlog::set_error_handler() or per-logger with logger::set_error_handler()
  • The registry stores the global handler and automatically attaches it to new loggers
  • Per-logger handlers override global handlers when both are present
  • Default behavior writes errors to stderr and continues execution
  • Handler signature is void(const std::string &msg), accepting any callable matching this type

Frequently Asked Questions

What happens if my error handler throws an exception?

spdlog catches exceptions in your handler and falls back to writing to stderr to prevent infinite recursion. Design your handler to not throw, or accept that secondary failures go to stderr.

Can I use the same handler for multiple loggers?

Yes. Handlers are stored as std::function objects, so you can assign the same callable to multiple loggers or share a std::function instance.

How do I restore the default error handler?

Set a handler that writes to stderr, or pass an empty lambda. There is no explicit "reset to default" API in the current spdlog version.

Does the error handler affect normal log message delivery?

No. The error handler only processes internal spdlog failures—sink write errors, formatting exceptions, and similar issues. Successfully formatted and delivered log messages never trigger the error handler.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →