A background task fails, the UI keeps running, and no report appears. Work through three separate questions: was EurekaLog active in that worker, who owns the exception, and who handles it? Creating a thread, enabling tracing and delivering a failure to the caller are separate actions.
Design the failure path to work without EurekaLog: stop the failed operation, release its resources, and notify the caller. Then add reporting at that boundary. Do not let the caller proceed as if a failed task succeeded simply because a dialog was shown.
For most new code, use TThreadEx or BeginThreadEx from EBase, pass an explicit activation
choice, and choose one exception-handling owner. For framework workers, adapt the framework's
completion or error callback instead. Chapter 6 gives the core calls; chapter 15 explains metadata
ownership and snapshots.
21.1 Where the processing happens, and what a thread carries
Exception processing can run in the background thread handling the exception, including EurekaLog event handlers. It is not automatically marshalled to the GUI thread, and several threads can process exceptions concurrently. Avoid direct GUI access and protect shared state appropriately.
For GUI applications, handling a completed worker's FatalException in the main thread is often
the clearest design (section 21.4). Consecutive processing serializes exception processing with
a global lock; it does not move it to the main thread or make shared application data thread-safe.
It also does not prevent reentrant exceptions on one thread while a dialog pumps messages.
The second thing a thread carries is state that has to be set per thread:
EurekaLog has to be enabled for background threads. A
thread is not covered because the application is covered - a thread that was never switched on produces
no report, however well the application itself is configured. The other piece of per-thread state is
the name, which the documentation says is shown in the debugger's Threads window and used as the
thread caption in bug reports. Both are set by two routines from EBase, a unit that can be included
in any application without the full EurekaLog code - which is what makes these calls safe in a build
that has no EurekaLog:
procedure NameThread(const AName: String; const AThreadID: Cardinal = 0); // EBase
function SetEurekaLogStateInThread(const AThreadID: Cardinal;
const AActivate: Boolean): Boolean; // EBase
The name is an arbitrary string - it does not have to be unique - and it may be set more than once,
the most recently set value winning. SetEurekaLogStateInThread switches EurekaLog on or off in one
thread (a thread id of 0 means the current thread) and returns the previous state, so the caller
can put it back instead of assuming what it was. This is per-thread state; the global flag is what
SetEurekaLogState changes, and the two are independent switches - EurekaLog starts up enabled for the
main thread alone, and a thread produces a report only when both are on. Chapter 6 covers the routines.
21.2 Choosing how to create the thread
CreateThread - the one the documentation tells you not to use
CreateThread is the basic system function for creating threads; every other method is built on it, and
the documentation warns against using it directly: at least BeginThread, and preferably
BeginThreadEx, because those functions contain the RTL thread support code. The reason is what happens
when the thread fails. A thread function passed to CreateThread is not protected by any exception
handler, and the documentation gives both endings: under the debugger, a "Project ... faulted
with message ..." notification and then termination of the application; in a free run, the
system's "... has stopped working..." dialog and then termination. EurekaLog installs a wrapper for any
CreateThread thread function to catch unhandled exceptions - as long as the "Use low-level hooks"
option is enabled and the corresponding threading option is set - but the wrapper changes only the
quality of the report, not the termination.
BeginThread - better, still not the recommendation
BeginThread is the RTL's basic function for creating threads from Delphi and C++ Builder code, a
simple wrapper for CreateThread, and the documentation recommends BeginThreadEx instead. Over the
system call it adds three things: it makes other RTL calls thread-safe (memory and heap operations, for
instance), it handles an unhandled thread exception by passing it to SysUtils.ShowException, and it
uses a more Pascal-friendly prototype. EurekaLog installs a wrapper for a BeginThread thread function
too, again provided low-level hooks are enabled, and again with the same ending.
Without those hooks you get BeginThread's own default handling, whose documented drawbacks are: a
low-level error dialog showing the raw exception address; termination of the application after an
exception in a thread; and unexpected behaviour outside the debugger, where a system error dialog is
followed by the SysUtils.ShowException dialog and possibly more dialogs during termination. Default
handling is therefore usually not good enough, and the instruction is to catch all exceptions in the
thread yourself - which is also what the RTL's BeginThread documentation asks for when it says the
thread function should handle all of its own exceptions.
BeginThreadEx - the recommended function
BeginThreadEx is one of the two helpers EurekaLog provides to simplify thread management (the other is
the TThreadEx class); both come from EBase and both are usable without EurekaLog enabled. Its
signature is BeginThread's with two arguments added at the end:
function BeginThreadEx(SecurityAttributes: Pointer; StackSize: LongWord; ThreadFunc: TThreadFunc;
Parameter: Pointer; CreationFlags: LongWord; var ThreadId: TThreadID;
const AThreadName: String = ''; const AEnableEL: Boolean = Boolean(2)): Integer; // EBase
The extra arguments name the thread and control its EurekaLog state. The state argument is ignored
without EurekaLog, but the name is not. Boolean(2) means inherit the current thread's state.
Pass True explicitly when reporting must not depend on the creator's per-thread state.
BeginThreadEx wraps the callback in an exception handler regardless of EurekaLog activation. An
unhandled exception invokes the default handler, produces a non-zero thread exit code and does
not itself terminate the application. With EurekaLog active, the handler uses its report processing;
without it, the default error handling still runs. Reporting does not undo partial application work.
Use EBase for the function and Windows for handle operations. Keep the returned handle if you
need to wait or read GetExitCodeThread; close it exactly once when you no longer need it.
Closing a handle does not cancel its thread. The callback keeps the BeginThread signature;
see chapter 6 for the complete creation/cleanup pattern.
The migration is easy: the thread function keeps the same signature as with BeginThread, which is
why the documentation suggests adding the "Ex" suffix to your existing BeginThread calls or running a
search-and-replace of BeginThread to BeginThreadEx. The exit code also becomes a success/failure
signal - non-zero when there was an exception, otherwise the thread function's result (usually zero) -
which is what GetExitCodeThread reads back from a handle you kept.
TThread - correct, but silent by design
TThread captures an exception escaping Execute in FatalException without invoking an error
handler by default. The thread object owns that exception until you explicitly transfer ownership.
A task can therefore fail quietly even though the exception was caught correctly by the RTL.
The Auto-handle TThread exceptions option changes this compatibility behavior. It can interfere
with third-party code that expects to consume FatalException, has no effect when EurekaLog is
disabled, and depends on low-level hooks. Prefer an explicit handling owner rather than turning the
option on to compensate for a missing completion path.
TThreadEx - the recommended class
TThreadEx descends from TThread and adds naming, activation and optional exception handling.
All five handling strategies in section 21.4 still apply. The named constructors' activation default
is Boolean(2), inheriting the creator's state. Pass an explicit Boolean when you need a
particular activation state rather than inheritance.
// Constructor forms for an existing TMyThread descendant of TThreadEx:
Thread := TMyThread.Create('Thread Name', True); // running, explicitly enabled
Thread := TMyThread.Create('Thread Name', False); // running, explicitly disabled
Thread := TMyThread.Create(True, 'Thread Name', True); // suspended; configure, then Start
Create suspended when setting AutoHandleException, OnTerminate, FreeOnTerminate or worker
inputs before execution. This startup use of "suspended" is not permission to suspend an arbitrary
running thread; section 21.6 explains the lock hazards of doing that.
In a TThreadEx.Execute override, call inherited Execute before your work. The API reference
requires it even though some introductory examples omit it. A long-running worker must also check
Terminated and cooperate with the application's shutdown protocol.
The class also supports anonymous threads through its own CreateAnonymousThread class method,
recommended over TThread.CreateAnonymousThread, and adds a property that saves the ceremony of an
OnTerminate handler: Thread.AutoHandleException := True; makes the class automatically invoke the
HandleException routine for the thread's FatalException. Processing happens in that worker,
not automatically in the caller or main thread. FatalException is not cleared by automatic
handling, so an owner can still inspect it to determine success; do not also submit it through a
second reporting path. A failed worker returns a non-zero exit code. With AutoHandleException
left at its default False, the class retains TThread's behavior.
This complete failure-path example belongs in a unit using SysUtils and EBase. Call
StartReportingProbe from a development-only action in an initialized EurekaLog application;
do not wire it into normal production startup. It intentionally fails once and reports in the worker.
uses
SysUtils, EBase;
type
TReportingProbe = class(TThreadEx)
protected
procedure Execute; override;
end;
procedure TReportingProbe.Execute;
begin
inherited Execute;
raise Exception.Create('Controlled background-thread failure');
end;
procedure StartReportingProbe;
var
Worker: TReportingProbe;
begin
Worker := TReportingProbe.Create(True, 'Reporting probe', True);
Worker.AutoHandleException := True;
Worker.FreeOnTerminate := True;
Worker.Start;
// Never read or free Worker after Start: it may already have destroyed itself.
end;
The expected result is one handled worker exception, identified as Reporting probe; dialog and
delivery behavior depend on the configured application policy. No report means you should trace
activation and the chosen handler, not enable every system thread. In real work, notify completion
through your application's lifetime-safe mechanism; this fire-and-forget probe is not a pattern for
recovering a task result.
Anonymous threads work the same way. An anonymous thread is a wrapper for TThread that
simplifies the "fire-and-forget" pattern and, in the documentation's words, has one additional
advantage: it closes over local variables, so you do not have to declare a thread class and copy the
values into it. Internally it is an instance of TAnonymousThread, a descendant of TThread, whose
Execute calls the supplied procedure, and it inherits TThread's error handling - with one exception:
since you did not declare a class, you cannot override DoTerminate. Two facts are easy to trip over:
anonymous threads are created in suspended state, so you must run them yourself with Start (which
is what allows you to set properties before running the thread), and they are created with
FreeOnTerminate set to True, which means - in the documentation's words - that you should
never access the thread variable after you have called Start.
Frameworks and thread pools
Threading frameworks - OmniThreadLibrary and AsyncCalls are the documentation's examples, and a
threading library can be part of a larger framework, as Indy's threading sub-library or JCL's
synchronisation primitives are - typically create threads you never see, so BeginThreadEx and
TThreadEx are usually not available. The job reduces to two tasks: enable EurekaLog for the threads
the framework creates (manually, via SetEurekaLogStateInThread; see section 21.3) and call EurekaLog's
exception handler for their unhandled exceptions. For the second, the documentation lists the common
shapes: if the framework calls the default exception handler (ApplicationHandleException) or
re-raises the exception in the caller thread, there is nothing to do; if it offers an
OnTerminate-like event, register your function there and analyse the termination state as section 21.4
does for TThread; otherwise wrap the thread code in an explicit try/except block and call the
handler yourself. A framework whose threads are also a thread pool requires the thread's state to be
preserved - one thread may serve many tasks, so priority, FPU state, thread name and EurekaLog's
per-thread state must be put back. For OmniThreadLibrary there is a documented extra: include the
EOTL unit, which the vendor ships in the installation's Source\Extras folder; thread names from
the library then reach EurekaLog while its internal service calls are filtered out of the call stack.
A thread pool reuses workers for multiple callbacks. Do not assume that a later task will run on the same thread, or that thread-local storage will be empty when your task starts. Do not terminate the pool's thread, leave its FPU/priority/activation state changed, or allow exceptions to escape the callback. Calls requiring a persistent dedicated thread need a different design. Restore every piece of thread state your task changes before returning control to the pool.
21.3 Switching EurekaLog on for the thread
EurekaLog offers two ways to get the per-thread activation flag set, and it recommends one of them emphatically. The automatic way is the "Default EurekaLog state in new threads" option, whose documented default is "Disabled (recommended)" and which can also be "Enabled" or "Enabled for RTL threads, disabled for Windows threads". Automatic enabling is highly not recommended, and the reason is a census of a real application: besides your own main thread and the one background thread you wrote, the process contains threads from the system thread pool, service threads for background tasks such as asynchronous WinSock work, threads created by the frameworks you use (Indy, for instance), and even threads created by the IDE for debugger tasks. Those were launched with some exception handling in mind, and their creator expects them to behave in a specific way - injecting EurekaLog processing and handling into every thread would be risky and unnecessary. Enable EurekaLog only for threads you know: threads you created, or that run your code, such as pool threads.
The manual way is therefore the rule - BeginThreadEx instead of BeginThread, TThreadEx instead of
TThread, and SetEurekaLogStateInThread where you cannot choose how the thread was made, thread pools
being the documentation's own example. Two cases are conceded to the automatic option: when you do not
want to change your code at all, and when you cannot control the thread's code but still want reports
from it - which works only where that code's own handler ends up calling the default exception handler
EurekaLog hooks. Even then, prefer "Enabled for RTL threads, disabled for Windows threads", and never
turn the option to "Enabled" until it is really needed.
For a dedicated BeginThread-style callback, name and enable the thread before the application
work begins, and handle failures at the callback boundary. The callback uses EBase,
Classes and SysUtils; its application work must follow the project's normal cleanup policy.
For a pool callback, preserve activation instead of unconditionally resetting it to False.
This helper is complete for a task that does not change the thread's name or other thread state.
Put it in a unit using EBase; pass an ordinary parameterless procedure (not an anonymous closure)
whose data remains valid until completion. The callback must use the Boolean result to record
task success or failure in its own completion mechanism.
type
TPoolWork = procedure;
function RunReportedPoolWork(const Work: TPoolWork): Boolean;
var
PreviousState: Boolean;
begin
PreviousState := SetEurekaLogStateInThread(0, True);
try
try
Work;
Result := True;
except
HandleException;
Result := False;
end;
finally
SetEurekaLogStateInThread(0, PreviousState);
end;
end;
False means the work raised and reporting was requested; it is not a successful task result.
As with any reporting boundary, an error in the handler itself still needs the framework's outer
failure policy. If the framework already reports callback exceptions, do not install a second
reporting owner: enable/restore activation around its normal task path instead.
In a dedicated TThread.Execute, the naming and activation calls belong before the actual thread
work. ApplicationHandleException(nil) from Classes handles the active exception from inside an
except block; HandleException from EBase also works without EurekaLog and can handle a saved
exception explicitly. Use the latter for FatalException when it has not been re-raised.
If a pooled task changes its thread name, save and restore the old name too, using the current thread's metadata (chapter 15). Do not reset it to an empty string unless that is genuinely the state the pool contract requires.
21.4 Getting the exception out of the thread
For a plain TThread - and therefore for anything derived from it, including TThreadEx with
AutoHandleException left off - an exception that escapes Execute is not raised anywhere by the time
you learn about it: it sits in FatalException, waiting to be read. The documentation lists five ways to
deal with a thread's exceptions, marking the first as recommended; the first four start from that
property, the fifth catches the exception inside Execute before it ever gets there. What separates the
five is which thread ends up processing the exception.
-
Re-raise it in the caller thread (recommended). Take the exception out of the thread object, clear the property so the thread object does not destroy it, and raise it in the caller, where the handler you already have deals with it. Two documented side effects: the caller's normal execution stops, so your code does not continue as if the thread had succeeded; and although the caller raises it, the recorded call stack is still the background thread's, even though that thread has terminated.
Thread.WaitFor; // analyse only after the thread has finished E := Thread.FatalException; PPointer(@Thread.FatalException)^ := nil; // clear, so it won't be deleted with the thread if Assigned(E) then raise E; if Thread.ReturnValue <> 0 then // no exception, but the thread still failed Abort;Here
Threadmust be a completed, caller-owned worker withFreeOnTerminate = False, andEis a localTObject. The owner must freeThreadin an outertry/finally. ClearingFatalExceptiontransfers ownership of the exception away from the thread; never freeEafter raising it. The pointer assignment is the documented low-level transfer technique, not a normal property setter. Do not use it while the worker is still running.WaitForbelongs only where blocking is acceptable. In a GUI, prefer completion notification orOnTerminateover waiting in an event handler; a raw wait can block UI messages, and a worker that needs main-thread synchronization makes the waiting design especially important. -
Handle it in the caller thread. Hand the stored exception to
HandleExceptionin place - useful when you want a report without disturbing the caller's flow - and abort the normal path yourself, or the caller carries on as if nothing had happened. A detail about which routine is usable here:ApplicationHandleExceptionalways analyses the active exception, so a saved, non-raised one has to go toHandleException. -
Handle it in an
OnTerminateevent handler. The documented handler runs withinSynchronize, from the main thread, so the exception is processed there. In anOnTerminate(Sender: TObject)handler withClassesandEBasein scope, use:if Assigned(TThread(Sender).FatalException) then HandleException(TThread(Sender).FatalException);. Do not free the exception in this form: the thread object still owns it. -
Handle it by overriding
DoTerminate. The same analysis, invoked by the class instead of an assigned event, with one difference the documentation emphasises:DoTerminateis called by a background thread and is not synchronised into the main thread, so the exception is processed by the thread itself. -
Handle it with an explicit
try/exceptblock inExecute. CallHandleExceptioninside theexceptblock and stop the failed work; preserve cleanup infinallyblocks. Processing then happens in the background thread.
One version limit is mentioned in passing: in Delphi 4 only method 5 is available, because TThread
there has no FatalException property at all. Two remarks apply to all five methods: the
documentation's own preference is TThreadEx, for the AutoHandleException described in section 21.2; and
EurekaLog has to be enabled for the background thread, or none of the handlers above will produce a
report - they still run, but without EurekaLog HandleException falls back to the standard error dialog.
One practical detail runs through the samples: when the thread is created with FreeOnTerminate
set to True - the norm for anonymous threads - the thread variable must not be touched after Start,
which is one more reason the OnTerminate handler and AutoHandleException appear so often.
21.5 What EurekaLog keeps for each thread
The name this chapter has been setting is one of the things EurekaLog keeps for a thread, and the unit
EThreadsManager exposes that bookkeeping as classes - the same place holds everything else EurekaLog
knows about the thread. The key objects are:
| Item | Purpose |
|---|---|
TEurekaThreadData |
Thread information. |
TEurekaThreadDataList |
List of threads. |
AcquireThreadData |
Returns thread information (any thread). |
ThreadData |
Returns thread information (current thread only). |
ThreadDataEnum |
Enumerates running threads. |
TThreadKind |
Indicates thread kind. |
The three accessors are not variations on one call: one answers for the thread you are in, one for an
arbitrary thread, and one for all of them at once - and they differ in whether what you get back is the
live object or a copy you have to release. Chapter 15, EThreadsManager: thread bookkeeping, catalogues
them. What matters here is the direction of the dependency: the name set with NameThread is what the
object carries and what a report prints as the thread's caption, and the same object supplies the
thread's identity and kind - which is how a report can still name the thread that failed instead of
listing a bare id.
21.6 The pitfalls, collected
Use these checks when reports disappear, shutdown stalls, or adding reporting changes timing:
- Never create a thread with
CreateThreadin an application you control. Its thread function is unprotected, and an unhandled exception there terminates the process. TThreadstores exceptions without reporting them by default. InspectFatalExceptionthrough a chosen completion path. The auto-handle compatibility option can interfere with that path; it is not a substitute for deciding who owns failure handling.- A thread's EurekaLog state is not the application's. EurekaLog has to be enabled for background threads, and not for every thread in the process: system pool threads, service threads, framework threads and IDE threads have their own exception-handling expectations.
- Low-level hook changes affect bookkeeping as well as timing. Without interception of
ExitThread, finished-thread records can remain until process shutdown unless the thread path performs EurekaLog's cleanup. Prefer the supportedTThreadEx/BeginThreadExwrappers. The internal routines_NotifyThreadGoneand_CleanupFinishedThreadsare not public integration points; do not insert guessed calls into arbitrary framework code. - Event handlers can run concurrently in background threads. Synchronize shared data and marshal GUI work. Consecutive processing only serializes reporting; it does not replace either rule.
- Pool threads are reused. Restore every state you change and route callback failures through the pool's error contract; never let an exception escape a raw system callback.
- Never touch the thread variable after
StartwhenFreeOnTerminateis set - use anOnTerminatehandler orAutoHandleExceptioninstead. - Suspending threads can cause the reporting deadlock you are investigating. Keep Pause all
EurekaLog-enabled/RTL/Windows threads during exception handling off unless you have a specific,
reviewed need. A suspended thread may own the memory manager lock; even allocating report text
can then block forever. Don't pause main thread is needed if handlers synchronize with it,
but it does not remove other lock dependencies. Do not confuse these options with
PauseFreezeCheck, which pauses checks rather than application threads. - Stop workers cooperatively before application finalization. A report or a thread handle being closed does not stop them. Terminate threads on shutdown can itself hang shutdown when it terminates a thread holding a global lock; it is not a cleanup design.
When the problem is thread-creation overhead, compare the same workload with the same build
and configuration, recording how often threads start and stop. KnowHow article 92 identifies
rapid thread churn as a diagnostic branch and recommends a pool for many short tasks. Temporarily
changing Advanced / Low-level injection hooks from Allow all to Allow for current module
only or Disabled can isolate hook-related cost; restore the setting afterward. A faster run
does not prove it is safe to ship with lost coverage or cleanup. Check whether internal diagnostic
logging is enabled too (chapter 5), rather than blaming ordinary ELogging entries indiscriminately.