NullPointerException
Event Handling
Software Debugging
Java Programming
Exception Management

Spontaneous NullPointerExceptions when firing Events

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Spontaneous NullPointerException (NPE) when firing events in Java occurs when an event listener is removed (unsubscribed) between the null check and the actual invocation. In a multi-threaded environment, one thread may check that the listener list is non-null, then a second thread removes the last listener, and the first thread throws an NPE when it tries to invoke on the now-null or empty list. This is a classic TOCTOU (time-of-check, time-of-use) race condition. The standard fix uses the "snapshot" pattern — copy the listener reference or list to a local variable before invoking.

The Race Condition

java
1public class EventSource {
2    private EventListener listener;
3
4    public void setListener(EventListener listener) {
5        this.listener = listener;
6    }
7
8    public void removeListener() {
9        this.listener = null;
10    }
11
12    public void fireEvent(String data) {
13        // Thread A checks: listener is not null
14        if (listener != null) {
15            // Thread B calls removeListener() — sets listener to null
16            listener.onEvent(data);  // NPE! listener is now null
17        }
18    }
19}

Between the if (listener != null) check and the listener.onEvent() call, another thread can set listener to null. The field is read twice (once for the check, once for the call), creating a window for the race condition.

Fix 1: Local Variable Snapshot

java
1public class EventSource {
2    private volatile EventListener listener;
3
4    public void setListener(EventListener listener) {
5        this.listener = listener;
6    }
7
8    public void removeListener() {
9        this.listener = null;
10    }
11
12    public void fireEvent(String data) {
13        // Copy to a local variable — this is a single read
14        EventListener localListener = listener;
15        if (localListener != null) {
16            localListener.onEvent(data);  // safe — local variable cannot be modified
17        }
18    }
19}

The local variable localListener captures the reference at a single point in time. Even if another thread sets listener = null afterward, localListener still holds the original reference. The volatile keyword ensures the read sees the latest value written by other threads.

Fix 2: CopyOnWriteArrayList for Multiple Listeners

java
1import java.util.List;
2import java.util.concurrent.CopyOnWriteArrayList;
3
4public class EventEmitter {
5    private final List<EventListener> listeners = new CopyOnWriteArrayList<>();
6
7    public void addListener(EventListener listener) {
8        listeners.add(listener);
9    }
10
11    public void removeListener(EventListener listener) {
12        listeners.remove(listener);
13    }
14
15    public void fireEvent(String data) {
16        // CopyOnWriteArrayList creates a snapshot for iteration
17        // Safe even if listeners are added/removed during iteration
18        for (EventListener listener : listeners) {
19            listener.onEvent(data);
20        }
21    }
22}

CopyOnWriteArrayList creates a new internal array on every write (add/remove). Iterators operate on a snapshot of the array at the time the iterator was created. This is the standard thread-safe pattern for event listener lists.

Fix 3: Synchronized Snapshot

java
1import java.util.ArrayList;
2import java.util.List;
3
4public class SynchronizedEventEmitter {
5    private final List<EventListener> listeners = new ArrayList<>();
6
7    public synchronized void addListener(EventListener listener) {
8        listeners.add(listener);
9    }
10
11    public synchronized void removeListener(EventListener listener) {
12        listeners.remove(listener);
13    }
14
15    public void fireEvent(String data) {
16        List<EventListener> snapshot;
17        synchronized (this) {
18            snapshot = new ArrayList<>(listeners);  // copy under lock
19        }
20        // Fire outside the lock to prevent deadlocks
21        for (EventListener listener : snapshot) {
22            listener.onEvent(data);
23        }
24    }
25}

Take a snapshot of the listener list inside a synchronized block, then iterate and invoke outside the lock. This prevents deadlocks where a listener callback tries to add/remove listeners.

C# Equivalent: The Null-Conditional Pattern

csharp
1// C# has the same race condition with events
2public class EventSource
3{
4    public event EventHandler<string> DataReceived;
5
6    // WRONG: race condition between null check and invocation
7    public void OnDataReceived(string data)
8    {
9        if (DataReceived != null)
10            DataReceived(this, data);  // NPE if unsubscribed between check and call
11    }
12
13    // CORRECT: local variable snapshot
14    public void OnDataReceivedSafe(string data)
15    {
16        var handler = DataReceived;  // snapshot
17        handler?.Invoke(this, data);  // null-conditional operator
18    }
19
20    // SIMPLEST: null-conditional operator directly
21    public void OnDataReceivedSimple(string data)
22    {
23        DataReceived?.Invoke(this, data);  // thread-safe in C#
24    }
25}

In C#, the ?. operator on an event delegate is thread-safe because it reads the delegate reference once and invokes it atomically. This is the idiomatic C# pattern.

Android/Kotlin Pattern

kotlin
1class EventEmitter {
2    private var listener: EventListener? = null
3
4    fun setListener(l: EventListener?) {
5        listener = l
6    }
7
8    fun fireEvent(data: String) {
9        // Kotlin's ?. is a snapshot + null check in one
10        listener?.onEvent(data)  // safe — reads listener once
11
12        // For multiple listeners
13        val listeners = listOf<EventListener>()  // snapshot from a thread-safe list
14        listeners.forEach { it.onEvent(data) }
15    }
16}

Common Pitfalls

  • Reading the listener field twice (check then use): The classic TOCTOU race. if (listener != null) listener.onEvent(data) reads the field twice. Between the reads, another thread can null the field. Always copy to a local variable first or use ?. (C#/Kotlin).
  • Iterating the listener list without a snapshot: If a listener callback adds or removes listeners from the same list during iteration, Java throws ConcurrentModificationException. Use CopyOnWriteArrayList or create a snapshot copy before iterating.
  • Firing events inside a synchronized block: If a listener callback synchronizes on the same lock (e.g., to remove itself), the call deadlocks. Always fire events outside the synchronized block by taking a snapshot inside and iterating outside.
  • Not using volatile on the listener field: Without volatile, the reading thread may see a stale cached value of the listener field due to CPU cache coherency. Mark the field volatile to ensure cross-thread visibility of the latest reference.
  • Assuming single-threaded environments: Even in applications that appear single-threaded (like Android UI), callbacks from background threads, timers, or lifecycle methods can modify listener references concurrently. Always use thread-safe event firing patterns as a defensive practice.

Summary

  • Spontaneous NPEs in events are caused by a race condition between null-checking and invoking the listener
  • Copy the listener reference to a local variable before checking and invoking (snapshot pattern)
  • Use CopyOnWriteArrayList for thread-safe multi-listener event emitters
  • In C#, use EventHandler?.Invoke() — the ?. operator is thread-safe for delegates
  • Fire events outside synchronized blocks to prevent deadlocks from listener callbacks
  • Mark listener fields as volatile to ensure cross-thread visibility

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.