android.os.NetworkOnMainThreadException means an Android application attempted a networking operation on its main UI thread. Android throws this exception for applications targeting the Honeycomb SDK/API level 11 or higher. The safest first action is not to suppress the exception: inspect the failing call path, identify the first app-owned blocking network operation, and determine whether the implicated client API is synchronous or already main-safe.

What android.os.NetworkOnMainThreadException means

The exact exception is android.os.NetworkOnMainThreadException. According to Android’s NetworkOnMainThreadException reference, it is thrown when an application attempts to perform networking on its main thread. It was added in API level 11 and is thrown for applications targeting the Honeycomb SDK or higher.

The triggering operation can be blocking URL connection work, URL.openStream(), socket access, name resolution, or a synchronous client call. Coroutine code can also trigger the exception when it invokes blocking I/O without changing to an appropriate background dispatcher. A function being marked suspend does not by itself move that work off the main thread.

Blocking the main thread prevents it from processing UI events while the operation runs, creating responsiveness and application-not-responding risk. Applications targeting earlier SDK levels were not subject to this exception in the same way, but that does not make main-thread networking safe or recommended.

Find the blocking network call in the failing path

Use the stack trace from the reproducible failure to locate the synchronous operation before changing execution models. Exact frame names and ordering vary by application and networking library, so this step requires project-specific confirmation.

  1. Reproduce the operation that reports android.os.NetworkOnMainThreadException and retain its complete stack trace.
  2. Read from the exception toward the application frames. Look for the first frame owned by your project that leads to URL connection work, URL.openStream(), socket access, name resolution, or a synchronous client call.
  3. Check the implicated library’s documentation to determine whether that method is blocking, callback-based, or a documented main-safe suspend API.
  4. Trace the caller’s execution context. Activity, fragment, composable callback and viewModelScope call paths commonly begin on the main thread, but the actual project path must be verified.
  5. Select one matching repair below rather than wrapping every network API in another execution mechanism.

A stack trace may show StrictMode$AndroidBlockGuardPolicy.onNetwork above an app-owned frame. This is a common diagnostic pattern, not a universal frame sequence. The useful frame is the project-owned call that initiated the blocking operation.

Use StrictMode only when additional debug diagnosis is needed

When it applies: Use StrictMode during local testing when the existing failure does not clearly reveal accidental network access on the main thread.

Prerequisites: Use a debug build or local test environment, and plan to remove or relax the diagnostic policy after verification.

  1. Enable StrictMode in the debug build.
  2. Trigger the failing flow.
  3. Inspect the reported violation and identify the first app-owned blocking network call.
  4. Correct the call’s thread ownership instead of suppressing the violation.

Expected result: The diagnostic identifies an app-owned path that can be assigned to the correct async API, dispatcher or Executor repair.

Risk and rollback: Risk is low and there is no expected data-loss risk. Disable the debug-only policy after the repair is verified. Do not use permitAll() or permitNetwork() as a production fix.

Choose the repair that matches the network API

Use the least invasive applicable path. First determine whether the client already performs asynchronous execution. Add a dispatcher or Executor only when the underlying operation is blocking.

Fix 1: Use an already-asynchronous or main-safe client API directly

When it applies: Use this path when the library documents that its callback or suspend API already handles background execution and is safe to call from main-thread code.

Prerequisites: Confirm the behavior in that client library’s documentation. Do not assume that every method marked suspend is main-safe.

  1. Prefer the library’s documented asynchronous callback or main-safe suspend API.
  2. Call that API from main-safe code.
  3. Do not add a redundant withContext(Dispatchers.IO) wrapper around an API already documented as main-safe.
  4. Resume UI work on the main thread after the operation completes.

Expected result: The library performs its network operation without blocking Android’s main thread, while the caller handles the result in the correct UI context.

Risk and rollback: Risk is low and there is no expected data-loss risk. If documentation or testing shows that the API is not actually main-safe, stop using this path and move the blocking layer off Main instead. Kotlin coroutines on Android distinguishes blocking code that needs Dispatchers.IO from networking APIs that already provide main-safe suspend behavior.

Fix 2: Move blocking Kotlin network work into withContext(Dispatchers.IO)

When it applies: Use this path for a Kotlin suspend function that directly invokes blocking network or name-resolution code.

Prerequisites: You must control the suspend function or repository layer, and the underlying operation must be blocking rather than already main-safe.

  1. Keep the caller on Main when it is responsible for UI state.
  2. Wrap only the blocking network block in withContext(Dispatchers.IO), or use the project’s injected I/O dispatcher.
  3. Return only the operation’s result to the caller.
  4. Update UI state after the suspend call completes back on Main.

Expected result: Blocking I/O runs on the I/O dispatcher, while UI state remains owned by the main thread.

Risk and rollback: Risk is low and there is no expected data-loss risk. Remove the dispatcher switch only if the library is confirmed to provide a main-safe suspend API. Android coroutine best practices requires suspend functions that may be called from Main to be main-safe and notes that viewModelScope normally begins on Dispatchers.Main.

Fix 3: Use a Java Executor and return the result to Main

When it applies: Use this path for Java code that directly performs blocking network I/O.

Prerequisites: The application must have a background Executor or thread pool and a main-thread callback path for UI updates.

  1. Submit the blocking network task to the Executor.
  2. Perform the network call within that background task rather than from the main thread.
  3. Post the result back through the application’s main-thread callback path.
  4. Update views only from that main-thread callback.

Expected result: The blocking operation runs through the Executor, and only the resulting UI work returns to Main.

Risk and rollback: Risk is low and there is no expected data-loss risk. Cancel the task or stop submitting new work when the initiating UI is gone. Moving the network operation off Main does not make it safe to update views from the background task.

Handle lifecycle and persistent work without creating a second bug

Correct thread ownership does not automatically make a request lifecycle-safe. A request can outlive its activity or fragment, return to a stale UI, or duplicate work after the initiating action is repeated. Cancellation, destruction handling and duplicate-request policy depend on the application’s architecture and must be verified in the target project.

  • Confirm what happens if the activity or fragment is destroyed while work remains active.
  • Ensure background work does not update views after the related UI is gone.
  • Verify whether repeated actions can submit duplicate requests.
  • Cancel work, or stop accepting its result, when the owning lifecycle no longer needs it.

Use WorkManager only for persistent or deferrable work

When it applies: Use WorkManager when the network task is deferrable, long-running, or should survive application restarts. It is not the default replacement for every immediate UI-triggered request.

Prerequisites: The operation must be suitable for expression as background work rather than an immediate request tied to the current screen.

  1. Create a WorkRequest or Worker for the background operation.
  2. Add network constraints if the task requires them.
  3. Let WorkManager run the operation away from the UI thread.
  4. Observe completion and update the UI separately.

Expected result: Persistent or deferrable work is managed independently of the initiating screen and does not block the main thread.

Risk and rollback: Risk is low and there is no expected data-loss risk. Cancel the WorkRequest when the task is no longer needed. If the request requires an immediate response for the current UI, use the applicable async API, coroutine or Executor path instead.

Verify that the exception is fixed

  1. Reproduce the original failing flow after applying one applicable repair path.
  2. Confirm that the blocking network or name-resolution operation no longer executes on the main thread.
  3. Confirm that view and UI-state updates occur only after the result returns to Main.
  4. Test what happens when the initiating activity or fragment is destroyed before completion.
  5. Test repeated input to determine whether duplicate requests are created or correctly controlled.
  6. In a debug build, use StrictMode to check for remaining accidental main-thread network access, then restore the intended debug policy.
  7. Review the resulting stack trace if another exception appears. Diagnose the new signature separately rather than treating it as another instance of NetworkOnMainThreadException.

The threading repair is verified when the original flow no longer performs blocking network work on Main and any related UI changes still occur on Main. A successful endpoint response alone does not prove that thread ownership is correct, and removing this exception does not guarantee that the network request itself will succeed.

Errors and workarounds that are not this fix

Message or approach Why it is different
UnknownHostException This is a different failure involving host resolution or reachability, not proof that network work ran on Main.
SSLHandshakeException This is a separate TLS failure and requires its own diagnosis.
Missing INTERNET permission This is a permission/configuration problem, not the defining cause of NetworkOnMainThreadException.
Cleartext-policy failure This is a network-security-policy restriction on cleartext traffic, not a main-thread execution violation.
CalledFromWrongThreadException This concerns view access from the wrong thread after work changes context; it is not the same exception.
permitAll() or permitNetwork() These relax the diagnostic restriction instead of correcting ownership of the blocking operation.
Deprecated AsyncTask guidance AsyncTask is deprecated and is not the preferred modern repair.

Do not begin with TLS, DNS reachability, permission or cleartext troubleshooting when the actual failure is android.os.NetworkOnMainThreadException. First correct thread ownership. If moving an HTTP request off Main then reveals a separate Cleartext HTTP traffic not permitted failure, diagnose that new error independently.