HTTP Status Codes: How to Distinguish Server Downtime vs Live Server Errors in AJAX Requests


2 views

When making AJAX requests from a mobile app, developers often need to distinguish between two critical scenarios:

  1. The server is actively responding but returning error codes (500-599)
  2. The server is completely unreachable (true downtime)

Contrary to some misconceptions, a completely offline server cannot return HTTP status codes. The 503 (Service Unavailable) scenario people mention only applies when:

  • The web server process is running
  • It's configured to return maintenance pages
  • But the application layer is intentionally disabled

In true server downtime situations, you'll encounter:

// JavaScript fetch example
try {
  const response = await fetch('https://api.example.com/data');
  if (!response.ok) {
    // Server responded with error status (4xx/5xx)
    console.log('Server error:', response.status);
  }
  // Process successful response
} catch (error) {
  // True connection failure scenarios:
  if (error instanceof TypeError) {
    // Network-level failure (DNS, CORS, etc.)
    console.log('Server unreachable - network error');
  } else {
    // Other exceptions
    console.log('Request failed:', error);
  }
}

Implement this dual-check approach:

async function checkServerStatus(url) {
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 5000);
    
    const response = await fetch(url, {
      signal: controller.signal,
      method: 'HEAD' // Lightweight request
    });
    
    clearTimeout(timeoutId);
    
    return {
      status: 'live',
      httpStatus: response.status
    };
  } catch (error) {
    return {
      status: 'down',
      errorType: error.name // 'AbortError', 'TypeError', etc.
    };
  }
}

For production apps, consider these additional factors:

  • DNS resolution failures throw different errors than TCP connection timeouts
  • Load balancers might return 502/503 while the origin server is down
  • Mobile networks have higher latency thresholds than desktop

Combine client-side detection with:

// Exponential backoff for retries
async function resilientRequest(url, retries = 3) {
  try {
    return await fetch(url);
  } catch (error) {
    if (retries === 0) throw error;
    const delay = Math.pow(2, 4 - retries) * 1000;
    await new Promise(res => setTimeout(res, delay));
    return resilientRequest(url, retries - 1);
  }
}

Remember that proper error handling requires understanding both HTTP semantics and network-layer failures. Always test with actual network disruption scenarios rather than just mock server errors.


When your mobile app makes AJAX requests, you'll encounter two fundamentally different error scenarios:

  • Live server returning error codes (50X responses)
  • Complete connection failure (no response at all)

Contrary to some misconceptions, a completely down server cannot return HTTP status codes. When the TCP stack isn't even responding, you'll get:

  • ECONNREFUSED in Node.js
  • URLError in Swift
  • Timeout exceptions in most languages

JavaScript detection pattern:


try {
  const response = await fetch('https://api.example.com/data');
  if (!response.ok) {
    // Server is live but returned error (500-599)
    console.log('Server error:', response.status); 
  }
} catch (error) {
  if (error.message.includes('Failed to fetch')) {
    // Complete connection failure
    console.log('Server is likely down'); 
  }
}

Android/Java equivalent:


try {
  HttpURLConnection connection = (HttpURLConnection) new URL(API_URL).openConnection();
  int status = connection.getResponseCode();
  // Handle status codes here
} catch (ConnectException e) {
  // Server unreachable
  Log.e("NETWORK", "Server appears down: " + e.getMessage());
}

While documentation suggests using 503 for maintenance, this requires:

  • A reverse proxy still running (e.g., Nginx)
  • Application-aware load balancer
  • Special maintenance mode infrastructure

True server crashes produce no HTTP response whatsoever.

For robust handling:


function checkServerStatus() {
  return new Promise((resolve) => {
    const timer = setTimeout(() => resolve('timeout'), 5000);
    
    fetch(HEALTH_CHECK_ENDPOINT)
      .then(res => {
        clearTimeout(timer);
        resolve(res.status === 200 ? 'healthy' : 'unhealthy');
      })
      .catch(() => {
        clearTimeout(timer);
        resolve('down');
      });
  });
}

Additional verification methods:

  • Ping the server IP
  • TCP port checks (netcat/telnet)
  • DNS resolution validation