How to Use __DEV__ Flag in NativeScript for Smarter Debug Logging

_DEV_ global magic variable

If you’ve ever shipped a NativeScript app and realized your console is still flooded with debug logs in production — this one’s for you.

NativeScript provides a handy global magic variable called __DEV__ that lets you conditionally run code only during development. In this article, I’ll show you how I used it to add verbose API logging that automatically disappears in production builds.

The Problem

While debugging an API integration in my NativeScript app, I needed to see the full request URL, parameters, and response body. So I added console.log everywhere:

const callApi = (endpoint: string, params: any) => {
  console.log(`POST ${endpoint}`)
  console.log(`params: ${JSON.stringify(params)}`)

  return fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
    body: JSON.stringify(params),
  }).then(async (response) => {
    const text = await response.text()
    console.log(`Response: ${response.status} ${text}`)
    return JSON.parse(text)
  })
}

This works great for debugging. But I definitely don’t want all of this logging in a production build — it’s noisy, can leak sensitive data, and adds unnecessary overhead.

I could manually comment/uncomment these lines every time I switch between debug and release… but there’s a much better way.

The Solution: __DEV__

NativeScript (via its Vite configuration) exposes several global magic variables that are resolved at build time:

VariableDescription
__DEV__true in development mode, false in production
__ANDROID__true when building for Android
__IOS__true when building for iOS
__VISIONOS__true when building for visionOS

The key insight is that __DEV__ is a compile-time constant, not a runtime check. This means:

  • ns debug android → __DEV__ is true
  • ns run android → __DEV__ is false
  • Release/production builds → __DEV__ is false

Even better, because it’s resolved at build time, Vite’s tree-shaking will completely remove the dead code branches from your production bundle.

Using __DEV__ in Practice

Here’s how I refactored my API utility:

const callApi = (endpoint: string, params: any) => {
  if (__DEV__) {
    console.log(`[API] >>> POST ${endpoint}`)
    console.log(`[API] >>> params: ${JSON.stringify(params)}`)
  }

  return fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
    body: JSON.stringify(params),
  }).then(async (response) => {
    // In production, parse JSON directly (faster, no extra memory allocation)
    if (!__DEV__) return response.json()

    // In development, read raw text first for debugging
    const text = await response.text()
    console.log(`[API] <<< ${response.status} ${text.substring(0, 500)}`)
    return JSON.parse(text)
  })
}

Now when I run ns debug android, I see detailed logs:

[API] >>> POST https://api.example.com/health_check
[API] >>> params: {"device_id":"abc123","secret":"xyz789"}
[API] <<< 200 {"status":"ok"}

And when I run ns run android or build for release — zero noise. The logging code doesn’t even exist in the bundle.

TypeScript Setup

If you’re using TypeScript (and you should be), you might see a squiggly line under __DEV__ because the compiler doesn’t know about it. Fix this by adding a type declaration.

Create or update your references.d.ts (or any .d.ts file included in your project):

declare const __DEV__: boolean
declare const __ANDROID__: boolean
declare const __IOS__: boolean
declare const __VISIONOS__: boolean

More Use Cases

The __DEV__ flag isn’t just for logging. Here are some other patterns I’ve found useful:

Mock Data in Development

export function getConfig() {
  if (__DEV__) {
    return {
      apiUrl: 'https://staging.example.com/api',
      timeout: 30000, // longer timeout for debugging
    }
  }
  return {
    apiUrl: 'https://api.example.com',
    timeout: 10000,
  }
}

Developer Tools / Debug UI

// In your main app component
if (__DEV__) {
  // Show a debug overlay with device info, network status, etc.
  registerDebugOverlay()
}

Performance Timing

export async function fetchData() {
  let start: number
  if (__DEV__) {
    start = Date.now()
  }

  const result = await api.getData()

  if (__DEV__) {
    console.log(`fetchData took ${Date.now() - start!}ms`)
  }

  return result
}

Skipping Expensive Operations During Development

if (__DEV__) {
  // Skip actual SMS sending during development
  console.log(`[DEV] Would send SMS to ${phoneNumber}: ${message}`)
  return { success: true }
}

return sendRealSms(phoneNumber, message)

How It Works Under the Hood

NativeScript uses Vite as its build tool. In the NativeScript Vite plugin, __DEV__ is defined using Vite’s define option, which performs a global string replacement at build time.

When you run ns debug, the build essentially does:

// Before (source code)
if (__DEV__) {
  console.log('debug info')
}

// After build (development)
if (true) {
  console.log('debug info')
}

And for production:

// After build (production) — before minification
if (false) {
  console.log('debug info')
}

// After tree-shaking/minification — completely removed!

The dead code is eliminated entirely, so there’s zero runtime cost in production.

Key Takeaways

  1. Use __DEV__ instead of manual flags or environment variables for debug/release branching
  2. It’s a compile-time constant — dead code gets tree-shaken from production builds
  3. Add TypeScript declarations to avoid type errors
  4. Great for: debug logging, mock data, dev tools, performance timing
  5. ns debug = __DEV__ is truens run / release = __DEV__ is false

Stop littering your codebase with // TODO: remove before release comments. Let the build tool do the work for you.