What is the difference between reqwest::Response::error_for_status and error_for_status_ref for HTTP error handling?

reqwest::Response::error_for_status and error_for_status_ref both convert HTTP error status codes (4xx and 5xx) into Rust Error values, but they differ in ownership semantics: error_for_status consumes the Response and returns Result<Response, Error>, while error_for_status_ref borrows the response and returns Result<&Response, Error>. The consuming variant allows access to the response body on success, while the borrowing variant preserves the response for further inspection even after the status check, which is useful when you need to read the error body from the server regardless of status code.

Basic error_for_status Usage

use reqwest::StatusCode;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::get("https://httpbin.org/status/404").await?;
    
    // error_for_status consumes the Response
    // Returns Result<Response, Error>
    match response.error_for_status() {
        Ok(response) => {
            println!("Success with status: {}", response.status());
            // Can use response body here
        }
        Err(e) => {
            // Error contains status information
            println!("Request failed: {}", e);
        }
    }
    
    // Response is consumed - can't use it after error_for_status
    // println!("{:?}", response); // Error: value borrowed after move
    
    Ok(())
}

error_for_status takes ownership of the response and returns it on success.

Basic error_for_status_ref Usage

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::get("https://httpbin.org/status/404").await?;
    
    // error_for_status_ref borrows the Response
    // Returns Result<&Response, Error>
    match response.error_for_status_ref() {
        Ok(_) => {
            println!("Success with status: {}", response.status());
        }
        Err(e) => {
            println!("Request failed: {}", e);
            // Response is still available for inspection
            println!("Status: {}", response.status());
            // Can still read the body
        }
    }
    
    // Response is still available after error_for_status_ref
    println!("Response status: {}", response.status());
    
    Ok(())
}

error_for_status_ref borrows the response, allowing continued use after the check.

Return Types Compared

use reqwest::Response;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::get("https://httpbin.org/get").await?;
    
    // error_for_status: consumes Response, returns Result<Response, Error>
    // fn error_for_status(self) -> Result<Response, Error>
    let result: Result<Response, reqwest::Error> = response.error_for_status();
    
    // error_for_status_ref: borrows Response, returns Result<&Response, Error>
    // fn error_for_status_ref(&self) -> Result<&Response, Error>
    let response2 = reqwest::get("https://httpbin.org/get").await?;
    let result: Result<&Response, reqwest::Error> = response2.error_for_status_ref();
    
    // Key difference:
    // - error_for_status: self -> Result<Self, Error>
    // - error_for_status_ref: &self -> Result<&Self, Error>
    
    Ok(())
}

The method signatures differ in ownership: self versus &self.

Reading Response Body After Error

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // error_for_status_ref allows reading body on error
    let response = reqwest::get("https://httpbin.org/status/500").await?;
    
    // Using error_for_status_ref to preserve response
    if let Err(e) = response.error_for_status_ref() {
        println!("Error: {}", e);
        
        // Can still read the response body
        let body = response.text().await?;
        println!("Error body: {}", body);
        
        // This is useful when servers return error details in the body
        // e.g., {"error": "database connection failed", "code": "E123"}
    }
    
    // Contrast with error_for_status
    let response = reqwest::get("https://httpbin.org/status/500").await?;
    match response.error_for_status() {
        Ok(_) => println!("Success"),
        Err(e) => {
            println!("Error: {}", e);
            // Cannot read body - response was consumed
            // let body = response.text().await?; // Error: value borrowed after move
        }
    }
    
    Ok(())
}

error_for_status_ref preserves the response for body reading on error.

Chaining with Body Reading

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // error_for_status enables clean chaining
    let body = reqwest::get("https://httpbin.org/get")
        .await?
        .error_for_status()?  // Consumes response, returns on error
        .text()                // Can only chain on success
        .await?;
    
    println!("Body: {}", body);
    
    // error_for_status_ref requires different pattern for body access
    let response = reqwest::get("https://httpbin.org/get").await?;
    response.error_for_status_ref()?;  // Only borrows
    
    // Now need to use response separately
    let body = response.text().await?;
    println!("Body: {}", body);
    
    Ok(())
}

error_for_status enables method chaining; error_for_status_ref requires separate steps.

Inspecting Error Response Details

use reqwest::StatusCode;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::get("https://httpbin.org/status/403").await?;
    
    // Use error_for_status_ref to inspect error response
    if let Err(e) = response.error_for_status_ref() {
        let status = response.status();
        
        println!("Error: {}", e);
        println!("Status: {} {}", status.as_u16(), status.canonical_reason().unwrap_or(""));
        
        // Read the body to get more details
        let body = response.text().await?;
        println!("Response body: {}", body);
        
        // Handle different error types
        match status {
            StatusCode::UNAUTHORIZED => {
                println!("Authentication required");
            }
            StatusCode::FORBIDDEN => {
                println!("Access denied");
            }
            StatusCode::NOT_FOUND => {
                println!("Resource not found");
            }
            StatusCode::INTERNAL_SERVER_ERROR => {
                println!("Server error");
            }
            _ => {
                println!("Other error: {}", status);
            }
        }
    }
    
    Ok(())
}

error_for_status_ref allows detailed error response inspection.

Pattern: Conditional Body Access

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::get("https://httpbin.org/status/404").await?;
    
    // Check status first with error_for_status_ref
    let is_success = response.error_for_status_ref().is_ok();
    
    if is_success {
        // Success path: consume response and read body
        // Can't do this directly - response is borrowed
        // Need to consume after the check
        let body = response.text().await?;
        println!("Success body: {}", body);
    } else {
        // Error path: read error body
        let status = response.status();
        let body = response.text().await?;
        println!("Error {} body: {}", status, body);
    }
    
    Ok(())
}

Use error_for_status_ref when you need different handling for success and error bodies.

Pattern: Error Response Logging

use reqwest::Response;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    async fn log_error_response(response: &Response) -> Result<String, reqwest::Error> {
        // Check status without consuming
        if let Err(e) = response.error_for_status_ref() {
            let status = response.status();
            let body = response.text().await?;
            
            // Log the error with full details
            eprintln!("HTTP Error: {} {}", status.as_u16(), status.canonical_reason().unwrap_or(""));
            eprintln!("Error body: {}", body);
            eprintln!("Error: {}", e);
            
            return Err(e);
        }
        
        // Success: read body
        response.text().await
    }
    
    let response = reqwest::get("https://httpbin.org/status/500").await?;
    let result = log_error_response(&response).await;
    
    if result.is_err() {
        println!("Request failed with logged details");
    }
    
    Ok(())
}

error_for_status_ref enables error logging with response body access.

Error Type Information

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::get("https://httpbin.org/status/404").await?;
    
    // Both methods return reqwest::Error on failure
    // The error contains:
    // - Status code (for HTTP errors)
    // - URL that was requested
    // - Whether it's a status error or other error
    
    match response.error_for_status_ref() {
        Ok(_) => println!("Success"),
        Err(e) => {
            // Error information
            println!("Error: {}", e);
            
            // Check if it's an HTTP status error
            if let Some(status) = e.status() {
                println!("HTTP status: {}", status);
            }
            
            // Get the URL
            if let Some(url) = e.url() {
                println!("URL: {}", url);
            }
            
            // Check error type
            if e.is_status() {
                println!("Status error");
            }
            if e.is_connect() {
                println!("Connection error");
            }
            if e.is_timeout() {
                println!("Timeout error");
            }
        }
    }
    
    Ok(())
}

Both methods return the same reqwest::Error type with full error context.

Unwrap Pattern

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // error_for_status can be used with ? for concise error propagation
    let body = reqwest::get("https://httpbin.org/get")
        .await?
        .error_for_status()?  // Propagates error, consumes response on success
        .text()
        .await?;
    
    println!("Body: {}", body);
    
    // error_for_status_ref requires response to remain borrowed
    // Can't use ? with subsequent operations that need ownership
    
    let response = reqwest::get("https://httpbin.org/get").await?;
    let _ = response.error_for_status_ref()?;  // Returns &Response
    
    // Response is still borrowed, but we need to read body
    // This works because text() takes ownership of Response
    // But we can't use it after error_for_status_ref with ?
    
    Ok(())
}

error_for_status works cleanly with the ? operator for method chaining.

Custom Client Configuration

use reqwest::Client;
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder()
        .timeout(Duration::from_secs(30))
        .build()?;
    
    // error_for_status works with custom clients
    let response = client
        .get("https://httpbin.org/status/200")
        .send()
        .await?;
    
    let body = response.error_for_status()?.text().await?;
    println!("Body: {}", body);
    
    // error_for_status_ref for error handling
    let response = client
        .get("https://httpbin.org/status/404")
        .send()
        .await?;
    
    if let Err(e) = response.error_for_status_ref() {
        println!("Error: {}", e);
        let error_body = response.text().await?;
        println!("Error body: {}", error_body);
    }
    
    Ok(())
}

Both methods work identically with custom reqwest::Client instances.

Response Reuse Pattern

use reqwest::Response;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    async fn process_response(response: &Response) -> Result<String, reqwest::Error> {
        // error_for_status_ref allows multiple operations on response
        response.error_for_status_ref()?;
        
        // Get headers before consuming body
        let content_type = response.headers()
            .get("content-type")
            .and_then(|v| v.to_str().ok())
            .unwrap_or("unknown");
        
        println!("Content-Type: {}", content_type);
        println!("Status: {}", response.status());
        
        // Now read body
        // Note: text() takes ownership, so we need response to be owned
        // This pattern works when response is passed by reference for checking
        
        Ok(format!("Processed response with status {}", response.status()))
    }
    
    let response = reqwest::get("https://httpbin.org/get").await?;
    let result = process_response(&response).await?;
    println!("{}", result);
    
    // Response still available for body reading after process_response
    let body = response.text().await?;
    println!("Body length: {}", body.len());
    
    Ok(())
}

error_for_status_ref enables multiple operations on the same response reference.

Status Code Handling

use reqwest::StatusCode;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // error_for_status and error_for_status_ref both handle 4xx and 5xx
    // as errors; 1xx, 2xx, and 3xx are considered success
    
    let statuses = [
        ("https://httpbin.org/status/200", "OK"),
        ("https://httpbin.org/status/201", "Created"),
        ("https://httpbin.org/status/301", "Redirect"),
        ("https://httpbin.org/status/400", "Bad Request"),
        ("https://httpbin.org/status/404", "Not Found"),
        ("https://httpbin.org/status/500", "Internal Server Error"),
    ];
    
    for (url, desc) in statuses {
        let response = reqwest::get(url).await?;
        let status = response.status();
        
        let result = response.error_for_status_ref();
        
        match result {
            Ok(_) => println!("{} ({}): Success", desc, status),
            Err(e) => println!("{} ({}): Error - {}", desc, status, e),
        }
    }
    
    Ok(())
}

Both methods treat HTTP 4xx and 5xx status codes as errors.

Error Unwrap vs Ref Pattern

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Pattern 1: error_for_status with early return
    let body = async {
        let response = reqwest::get("https://httpbin.org/get").await?;
        let response = response.error_for_status()?;  // Consumes on success
        let body = response.text().await?;
        Ok::<_, reqwest::Error>(body)
    }.await?;
    
    // Pattern 2: error_for_status_ref with conditional
    let response = reqwest::get("https://httpbin.org/get").await?;
    if response.error_for_status_ref().is_ok() {
        let body = response.text().await?;
        println!("Body: {}", body);
    } else {
        let error_body = response.text().await?;
        println!("Error body: {}", error_body);
    }
    
    // Pattern 3: error_for_status_ref with ? for references
    fn check_status(response: &reqwest::Response) -> Result<(), reqwest::Error> {
        response.error_for_status_ref()?;
        Ok(())
    }
    
    let response = reqwest::get("https://httpbin.org/get").await?;
    check_status(&response)?;
    // Response still available
    
    Ok(())
}

Choose based on whether you need the response after the status check.

Synthesis

Method comparison:

Aspect error_for_status error_for_status_ref
Ownership Consumes Response Borrows &Response
Return type Result<Response, Error> Result<&Response, Error>
After error Response consumed Response still available
Method chaining Clean (?.text()) Requires separation
Body access on error Not possible Possible

Use cases:

Scenario Recommended Method
Simple success path error_for_status
Method chaining with ? error_for_status
Reading error body error_for_status_ref
Logging error details error_for_status_ref
Inspecting headers on error error_for_status_ref
Function taking &Response error_for_status_ref

Status code handling:

Status Code Result
1xx (Informational) Ok
2xx (Success) Ok
3xx (Redirect) Ok
4xx (Client Error) Err
5xx (Server Error) Err

Key insight: The difference between error_for_status and error_for_status_ref is purely about ownership semantics. error_for_status takes ownership of the Response, returning it on success and enabling clean method chaining (response.error_for_status()?.text().await?). error_for_status_ref borrows the response, preserving it for subsequent operations—essential when you need to read the response body on error, log error details, or inspect headers regardless of the status code. The underlying error detection logic is identical: both methods check if the status code indicates an HTTP error (4xx or 5xx) and return the same reqwest::Error type. Use error_for_status when you only care about success and want concise chaining; use error_for_status_ref when error responses contain valuable information that needs to be logged or processed.