Skip to content

Recommended Reading

React Native Logging: From console.log to Production

12 Minutes

React Native Logging: From console.log to Production

Fix Bugs Faster! Log Collection Made Easy

Get started

console.log is the foundation of logging in React Native. It gives you immediate feedback, it’s easy to use and it integrates with several developer tools. But if you want continuous visibility throughout your app’s lifecycle, console.log only takes you part of the way there.

Once your app ships, you lose the convenient development environment that makes console output easy to inspect, and you want have automatic access to the logs. That’s why we’ve put this guide together: to bridge the visibility gap and extend your coverage out into the real world.

  • First you’ll see how to view logs in development with Metro, DevTools, and the native tools.
  • Then we’ll show how to set up react-native-logs with custom levels and transports.
  • Finally we’ll show you how to write structured logs and send them to a file or a remote server.

How logging works in React Native

React Native executes your app’s JavaScript code in a separate engine, usually Hermes. console.log allows you to see what that code is doing at runtime.

  • console.log writes messages from your application’s JavaScript runtime to the development logging infrastructure.
  • Once there, you can inspect the messages in your terminal or developer tools.
  • React Native’s development environment provides the mechanisms that make those messages accessible.

The important thing to note is that while the output flows to your terminal and developer tools during development, in production the output goes wherever you send it – or nowhere if you send it nowhere.

Why console.log is not enough in production

console.log isn’t designed to be a production logging strategy. It doesn’t inherently provide log persistence, centralized collection, or reliable access to logs from users’ devices.

Once your app leaves your local environment, console.log has three specific limitations:

  • The output has nowhere to go. Once the app leaves your terminal, there is no console for you to read, so logs on a user’s device are invisible.
  • The messages are unstructured strings. Plain text cannot be queried or filtered by field, so you need to scan everything to find a single event.
  • Leftover logs can hurt performance. Excessive console calls in a release build can slow the app, especially during heavy renders.

The core problem is visibility. When a user hits a bug on their phone, no amount of console.log helps, simply because you are not there to read the console.

Viewing logs in development

When your app is in development, the console methods work much as they do in a browser, and each carries a severity level you can filter on later:

console.log("App started");
console.warn("Token expiring soon");
console.error("Payment request failed");

There are three places to read that output while building, from quickest to most detailed.

Inspecting logs in React Native DevTools

In modern React Native versions, console output is primarily viewed through React Native DevTools. They give you a full console panel, where you can filter by level and expand the objects you want to see.

  • With the dev server running, press j in the Metro terminal to open it.
  • Once you’ve done this, select the Console tab.

Some methods, like console.table and console.dir, only render here, not in the plain terminal.

Reading logs in the Metro terminal

Metro is React Native’s JavaScript bundler and development server. In older React Native versions, JavaScript console messages were forwarded to the Metro terminal.

If you’re using an older React Native version or a framework that forwards logs to the terminal, you may still see them alongside Metro’s output. But for current development workflows, it’s best to use React Native DevTools as your primary JavaScript logging interface.

Streaming native logs with the CLI

When a problem originates in the native Android or iOS layer rather than your application’s JavaScript code, you need access to native device logs. The React Native CLI provides commands for viewing native logging output while your app is running:

npx react-native log-android
npx react-native log-ios

Use these if crash or error messages aren’t reaching your JavaScript console. You’ll find that the CLI surfaces native-layer messages the other two tools miss.

For the debugging side of this, see our guide to React Native debugging.

Getting logs out of a WebView

If your app renders a WebView, its JavaScript runs in a separate context entirely, so console.log calls inside that WebView never reach Metro or React Native DevTools. This is a common source of confusion: your React Native logs look fine, but you’re debugging blind inside the WebView itself.

The fix is bridging the WebView’s console to React Native’s, using onMessage:

<WebView
  source={{ uri: "<https://example.com>" }}
  injectedJavaScript={`
    console.log = (...args) => {
      window.ReactNativeWebView.postMessage(JSON.stringify(args));
    };
    true;
  `}
  onMessage={(event) => {
    log.info("WebView log", JSON.parse(event.nativeEvent.data));
  }}
/>

The injected script overrides console.log inside the WebView by replacing it with a custom function that sends each call’s arguments to React Native through window.ReactNativeWebView.postMessage().

The React Native onMessage handler receives those messages and passes them to your logger, making WebView logs accessible alongside your other application logs.

How to set up a logging library

A logging library is a great first step to ensuring observability as your app enters production. It gives you a configurable logging pipeline with severity filtering, formatting, and transports. Crucially, it also enables you to route logs somewhere other than the terminal.

The react-native-logs library is a popular choice, built for React Native and Expo with no Node dependencies.

Installing and configuring react-native-logs

You can get a basic logger running in two simple steps:

  1. Install the library with your package manager: npm install react-native-logs.
  2. Create a logger instance and start using its logging methods.
import { logger } from "react-native-logs";

const log = logger.createLogger();

log.debug("This is a debug message");
log.info("This is an info message");
log.error("This is an error message");

Setting custom levels, colors, and namespaces

You can define your own levels by severity and colorize the console output while you code:

const log = logger.createLogger({
  levels: { debug: 0, info: 1, warn: 2, error: 3 },
  severity: "debug",
  transportOptions: {
    colors: { info: "blueBright", warn: "yellowBright", error: "redBright" }
  }
});

Namespaces scope logs to parts of the app, so you can enable only the ones you are investigating:

const homeLog = log.extend("HOME");
homeLog.info("Home screen loaded");

Switching transports for development and production

A transport is the function that displays, saves, or sends each log, and manipulating this function is crucial to ensuring visibility of real user devices. By switching transport according to its environment, you can log to the console while developing and to a file or server in production:

import { logger, consoleTransport, fileAsyncTransport } from "react-native-logs";
import RNFS from "react-native-fs";

const log = logger.createLogger({
  transport: __DEV__ ? consoleTransport : fileAsyncTransport,
  severity: __DEV__ ? "debug" : "error",
  transportOptions: { FS: RNFS, fileName: "log.txt" }
});

The __DEV__ flag is true in development and false in release builds, so this keeps noisy logs out of production while still capturing errors.

How to write structured logs in React Native

A structured log records named fields instead of a sentence, which is what lets you query logs later instead of simply scanning text.

In react-native-logs, you get structure by passing objects rather than strings. The logger joins the message and data together:

const errorObject = { status: 404, message: "Undefined Error" };
log.error("Request failed", errorObject);

Logging an object attaches the fields you will want to filter on later, like a user ID, screen, or request status.

For the fundamentals of the format itself, see our guide to structured logging.

How to save logs to a file

Writing logs to a local file gives you a persistent record of application activity that can be inspected after the app closes or encounters a problem. Because logs are written asynchronously, the most recent messages might not always be saved if the app terminates unexpectedly.

The fileAsyncTransport handles this, using either react-native-fs or Expo’s file system:

import { logger, fileAsyncTransport } from "react-native-logs";
import RNFS from "react-native-fs";

const log = logger.createLogger({
  transport: fileAsyncTransport,
  transportOptions: {
    FS: RNFS,
    fileName: "app_logs_{date-today}.txt"
  }
});

log.info("Saved to a daily log file");

Tip: Use {date-today} in the filename. This rolls the log over daily, so you get a fresh file each day rather than one humongous file that grows forever.

How to send logs to a remote server

No matter how well your logs are written, you won’t be able to see them if they only live on your user’s devices. To actually see production logs, you need to send them off the device. react-native-logs does this through a custom transport.

Writing a custom transport

A transport is just a function that receives each log and does something with it.

To send logs to your own endpoint:

  1. Write a transport function that takes the log props and posts them with fetch.
  2. Pass it to createLogger as the transport option.
const remoteTransport = (props) => {
  fetch("<https://example.com/logs>", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ level: props.level.text, msg: props.rawMsg })
  });
};

const log = logger.createLogger({ transport: remoteTransport });

This posts every log to a server you control, where you can store and search it.

Sending logs to a logging service

Rolling your own endpoint means building and maintaining the ingestion, storage, and search yourself. A logging service handles that for you, and react-native-logs ships presets for several.

The same custom-transport pattern is how a dedicated remote logging tool like Bugfender receives logs, so you get the collection, storage, and searchable interface without building the backend.

How to log network requests

Network problems are a common source of bugs that are hard to reproduce, so it’s important to handle logging requests and their outcomes deliberately. Log the request as it goes out and the response or error as it returns, with the fields you will want to filter on:

try {
  const res = await fetch(url);
  log.info("Request complete", { url, status: res.status });
} catch (err) {
  log.error("Request failed", { url, err });
}

Capturing the URL and status as fields lets you later find every failed request to one endpoint, which is far harder with a plain message.

What to never log in a mobile app

Data like keys, passwords and payment details must never reach a log, on any platform. The risk is even greater on mobile, because logs can easily end up in local files or remote services where sensitive values persist.

Never log thisWhy it is dangerous
Passwords and secretsTypically reused across services, so one leaked log can compromise more than your app
API keys and tokensGrant access to your backend or third-party services if exposed
Access and session tokensLet an attacker impersonate the user’s authenticated session
Full payment detailsCard numbers and CVVs fall under PCI rules and cause direct financial harm
Personal identifiable informationNames, emails, and IDs trigger GDPR and privacy obligations
Health or financial recordsRegulated data with strict handling requirements and heavy penalties

When you need to confirm a value without exposing it, log a hash of it instead of the value itself. For more analysis on this topic, see our guide on what not to log.

React Native logging best practices

A few habits will keep your React Native logging useful without letting it slow the app or leak data.

  • Log objects, not strings: pass fields you can filter on later, instead of baking everything into one message.
  • Switch transports by environment: console in development, file or remote in production, gated on __DEV__.
  • Set a production severity floor: logging only warnings and errors in release builds will keep your volume and cost down.
  • Log asynchronously: enable the library’s async mode, so logging does not block the UI thread during heavy renders.
  • Keep sensitive data out: mask or hash anything private before it is logged.
  • Use namespaces: scope logs by feature, so you can enable only the part you are investigating.

And finally… using a dedicated logging service

Building your own remote logging endpoint means taking responsibility for log ingestion, storage, search, and delivery reliability. A dedicated logging service provides this infrastructure without requiring you to build and maintain it yourself.

At this point we wanted to mention our own tool, Bugfender, because it’s specifically developed for remote logging. Bugfender gathers logs and exceptions from real devices in production and organizes them by:

  • User: trace one person’s path to a particular bug.
  • Session: see the full sequence of a single app run.
  • Device: isolate issues tied to an OS version or model.

Try Bugfender free and start collecting logs from real devices in minutes.

Frequently asked questions

Why are my console logs not showing in React Native?

The most common cause is that logs move to a different place than you expect. In newer React Native, JavaScript logs appear in React Native DevTools rather than Metro, and some methods like console.table never show in the terminal. Check DevTools, and confirm your log level is not being filtered out.

How do you see logs in release mode?

Release builds strip most console output, so you need logs written somewhere persistent. Use a file transport or a remote logging service that runs in production, gated so it stays active in release mode while development-only logging is disabled. Without one, logs from a release build are effectively invisible.

Revised: “How to collect logs from real user devices” (folds in “client logs” phrasing naturally, no structural change)

How do you view logs from a user’s device remotely?

A remote logging tool integrates an SDK that transmits logs over the network to a dashboard you can search, so you see events from real users without physical access. This is the only way to inspect what happened on a device you do not hold.

What log levels should a React Native app use?

Four cover most needs: debug for development detail, info for normal events, warn for recoverable problems, and error for failures. Set a higher minimum in production, such as warn or error, so release builds stay quiet while still capturing what matters.

Does logging slow down a React Native app?

Excessive synchronous logging can cause frame drops during animations or heavy renders. Enable the library’s async mode so logging defers until the UI thread is idle, keep production levels high, and avoid logging on hot paths. Done that way, the performance impact is negligible.

Expect The Unexpected!

Debug Faster With Bugfender

Start for Free
blog author

Aleix Ventayol

Aleix Ventayol is CEO and co-founder of Bugfender, with 20 years' experience building apps and solutions for clients like AVG, Qustodio, Primavera Sound and Levi's. As a former CTO and full-stack developer, Aleix is passionate about building tools that solve the real problems of app development and help teams build better software.

Join thousands of developers
and start fixing bugs faster than ever.