Mobile Development 10 min read

Why Does Creating a Handler in a Background Thread Throw an Exception?

This article explains Android's Handler and Looper mechanism, shows why creating a Handler in a non‑Looper thread triggers a RuntimeException, and walks through the relevant source code—including Handler constructors, Looper.myLooper(), ThreadLocal operations, and the required Looper.prepare() call.

AI Code to Success
AI Code to Success
AI Code to Success
Why Does Creating a Handler in a Background Thread Throw an Exception?

Background

Android provides Handler and Looper for intra‑process thread communication. A Handler posts work to a thread that owns a Looper, allowing long‑running tasks to run off the UI thread while UI updates stay on the main thread.

Why a Handler is needed

Running a time‑consuming operation on the main thread triggers an ANR. Offloading to a background thread via a Handler keeps the UI responsive, but UI objects may only be accessed from the thread that owns the Looper (usually the main thread). Handlers enable posting back to that thread.

Problem: Creating a Handler in a child thread without a Looper

The following code crashes at runtime:

new Thread(new Runnable() {
    @Override
    public void run() {
        new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
            }
        };
    }
}, "MyThread").start();

Exception:

Can't create handler inside thread that has not called Looper.prepare()

Handler constructor flow

The default constructor delegates to the two‑argument constructor, which obtains the Looper of the current thread:

public Handler() {
    this(null, false);
}

public Handler(Callback callback, boolean async) {
    mLooper = Looper.myLooper();   // (1)
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

If Looper.myLooper() returns null, the constructor throws the RuntimeException shown above.

How Looper.myLooper() works

Looper.myLooper()

simply returns the Looper stored in a thread‑local variable:

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

The thread‑local storage is managed by ThreadLocal. Its get() method looks up the current thread’s ThreadLocalMap and returns the value associated with the Looper key, or null if none exists.

ThreadLocal.get()

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
            return result;
        }
    }
    return setInitialValue();   // default returns null
}

Creating a Looper with Looper.prepare()

Calling Looper.prepare() creates a new Looper instance and stores it in the thread’s ThreadLocal:

public static void prepare() {
    prepare(true);
}

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}
ThreadLocal.set()

either updates an existing map or creates a new one:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}
createMap()

allocates a fresh ThreadLocalMap and stores the Looper as the value for the current ThreadLocal key.

Putting it together

When a background thread needs a Handler, the sequence is:

Call Looper.prepare() once in that thread.

Create the Handler (the constructor will find the Looper via Looper.myLooper()).

Optionally start the message loop with Looper.loop() if you intend to process messages.

If Looper.prepare() is omitted, Looper.myLooper() returns null, causing the RuntimeException shown earlier. Only one Looper can exist per thread; a second call to Looper.prepare() throws “Only one Looper may be created per thread”.

Example of correct usage

Handler handler;
Thread worker = new Thread(() -> {
    Looper.prepare();               // create Looper for this thread
    handler = new Handler(Looper.myLooper()) {
        @Override
        public void handleMessage(Message msg) {
            // process message
        }
    };
    Looper.loop();                  // start message loop
});
worker.start();

Diagram

Handler‑Looper relationship diagram
Handler‑Looper relationship diagram
AndroidConcurrencyANRLooperThreadLocalHandler
AI Code to Success
Written by

AI Code to Success

Focused on hardcore practical AI technologies (OpenClaw, ClaudeCode, LLMs, etc.) and HarmonyOS development. No hype—just real-world tips, pitfall chronicles, and productivity tools. Follow to transform workflows with code.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.