Admin SDK error handling

Admin SDK errors are divided into two categories:

  1. Programming errors: These are programming and configuration errors in the user application. They mostly occur due to the incorrect usage of the SDK (such as passing null to a method that doesn’t accept null values), and other configuration errors at the Firebase project or SDK level (missing credentials, incorrect project ID string, and so on).
  2. API errors: These include various recoverable errors that occur within the SDK implementation, all the errors that originate in Firebase backend services, and other transient errors (such as timeouts) that may occur while making RPC calls.

The Admin SDK signals programming errors by throwing an error that is native to the platform in question.

  • Java: Throws instances of IllegalArgumentException, NullPointerException or similar built-in runtime error type.
  • Python: Raises instances of ValueError, TypeError or other built-in error type.
  • Go: Returns a generic error.
  • .NET: Throws instances of ArgumentException, ArgumentNullException or similar built-in error type.

In most situations you should not explicitly handle programming errors. Instead, you should fix your code and configuration to avoid programming errors altogether. Consider the following Java snippet:

String uid = getUserInput();

UserRecord user = FirebaseAuth.getInstance().getUser(uid);

If the getUserInput() method returns null or empty strings, the FirebaseAuth.getUser() API throws an IllegalArgumentException. Instead of explicitly handling it, you can mitigate the issue by ensuring the getUserInput() method never returns an invalid UID string. If that’s not possible, implement the necessary argument checking in your own code as follows:

String uid = getUserInput();
if (Strings.isNullOrEmpty(uid)) {
    log.warn("UID must not be null or empty");
    return;
}

UserRecord user = FirebaseAuth.getInstance().getUser(uid);

As a principle, never retry on programming errors. Allowing for fail-fast semantics on programming errors is often the best course of action because it exposes programming bugs and configuration errors during development, where they can be promptly fixed. Fail-fast in this context can mean letting the errors propagate to a global error handler in your application, or just logging them for audit purposes followed by the termination of the current execution flow (the application should not have to crash). In general, follow the error handling best practices of your programming language and the application framework. This alone is often sufficient to correctly deal with this class of errors.

Typically, the bulk of your error handling efforts will focus on handling API errors. Some of these errors are recoverable, such as errors that resulted from a temporarily unavailable service, and some are even anticipated during the normal program execution flow, such as detecting invalid or expired ID tokens. The rest of this guide outlines how the Admin SDK represents such API errors, and the various options that are available for handling them.

Structure of an API error

An API error consists of the following components:

  1. Error code
  2. Error message
  3. Service error code (Optional)
  4. HTTP response (Optional)

Every API error is guaranteed to contain an error code and an error message. Certain API errors also contain a service error code that is specific to the API that generated the error. For example some errors generated by the Firebase Auth API contain a service error code that is specific to Firebase Auth. If the error was the result of an HTTP error response from a backend service, the API error also contains the corresponding HTTP response. This can be used to inspect the exact headers and contents of the original response, which is useful for debugging, logging, or implementing more sophisticated error handling logic.

All Admin SDK implementations except Node.js provide APIs that enable accessing the above components of API errors.

Error types and APIs by language

Java

In Java all API errors extend the FirebaseException class. You can access the error code, error message and the optional HTTP response from this base class.

public class FirebaseException extends Exception {
    @NonNull
    public ErrorCode getErrorCode() {
        // ...
    }

    @NonNull
    public String getMessage() {
        // ...
    }

    @Nullable
    public IncomingHttpResponse getHttpResponse() {
        // ...
    }
}

APIs that expose service error codes provide API-specific subclasses of FirebaseException. For example all public methods in the FirebaseAuth API are declared to throw instances of FirebaseAuthException. You can access the service error code from this derived class.

public class FirebaseAuthException extends FirebaseException {
    @Nullable
    public AuthErrorCode getAuthErrorCode() {
        // ...
    }
}

Python

In Python all API errors extend the exceptions.FirebaseError class. You can access the error code, the error message, and the optional HTTP response from this base class.

class FirebaseError(Exception):
    @property
    def code(self):
          # ...

    @property
    def message(self):
          # ...

    @property
    def http_response(self):
          # ...

Moreover, Python Admin SDK offers separate derived classes for each error code. We refer to them as platform error classes.

class InvalidArgumentError(FirebaseError):
    # ...

class NotFoundError(FirebaseError):
    # ...

You can either catch FirebaseError in your code and check its code, or perform an isinstance() check against a platform error class. Or you can write code to directly catch specific platform error types. The latter approach is likely to result in more readable error handling code.

APIs that expose service error codes provide API-specific subclasses of platform error classes. For example, all public methods in the auth module may throw API-specific error types such as auth.UserNotFoundError and auth.ExpiredIdTokenError.

class UserNotFoundError(exceptions.NotFoundError):
    # …

class ExpiredIdTokenError(exceptions.InvalidArgumentError):
    # ...

Go

The Go Admin SDK provides an errorutils package which contains a series of functions that allow testing for error codes.

package errorutils

func IsInvalidArgument(err error) bool {
    // ...
}

func IsNotFound(err error) bool {
    // ...
}

The error message is simply the string returned by the Error() function of an error. The optional HTTP response can be accessed by calling the errorutils.HTTPResponse() function, which returns an *http.Response.

It is safe to pass nil or any other error value to the error checking functions in the errorutils package. They return true if the input argument actually contains the error code in question, and return false for everything else. The HTTPResponse() function has similar behavior, except it returns nil instead of false.

APIs that expose service error codes provide API-specific error checking functions in the corresponding packages. For example, the auth package provides the functions IsUserNotFound() and IsExpiredIDTokenError().

.NET

In .NET all API errors extend the FirebaseException class. You can access the platform error code, error message and the optional HTTP response from this base class.

public class FirebaseException : Exception {

    public ErrorCode ErrorCode { get; }

    public String Message { get; }

    public HttpResponseMessage HttpResponse { get; }
}

APIs that expose service error codes provide API-specific subclasses of FirebaseException. For example, all public methods in the FirebaseAuth API are declared to throw instances of FirebaseAuthException. You can access the service error code from this derived class.

public class FirebaseAuthException : FirebaseException {

    public AuthErrorCode AuthErrorCode { get; }
}

Platform error codes

Error codes are common across all Firebase and Google Cloud Platform services. The following table outlines all the possible platform error codes. This is a stable list, and is expected to remain unchanged for a long period.

INVALID_ARGUMENT Client specified an invalid argument.
FAILED_PRECONDITION Request cannot be executed in the current system state, such as deleting a non-empty directory.
OUT_OF_RANGE Client specified an invalid range.
UNAUTHENTICATED Request not authenticated due to missing, invalid or expired OAuth token.
PERMISSION_DENIED Client does not have sufficient permission. This can happen because the OAuth token does not have the right scopes, the client does not have permission, or the API has not been enabled for the client project.
NOT_FOUND Specified resource not found, or the request is rejected due to undisclosed reasons such as whitelisting.
CONFLICT Concurrency conflict, such as read-modify-write conflict. Only used by a few legacy services. Most services use ABORTED or ALREADY_EXISTS instead of this. Refer to the service-specific documentation to see which one to handle in your code.
ABORTED Concurrency conflict, such as read-modify-write conflict.
ALREADY_EXISTS The resource that a client tried to create already exists.
RESOURCE_EXHAUSTED Either out of resource quota or reaching rate limiting.
CANCELLED Request cancelled by the client.
DATA_LOSS Unrecoverable data loss or data corruption. The client should report the error to the user.
UNKNOWN Unknown server error. Typically a server bug.

This error code is also assigned to local response parsing (unmarshal) errors, and a wide range of other low-level I/O errors that are not easily diagnosable.

INTERNAL Internal server error. Typically a server bug.
UNAVAILABLE Service unavailable. Typically the server is temporarily down.

This error code is also assigned to local network errors (connection refused, no route to host).

DEADLINE_EXCEEDED Request deadline exceeded. This will happen only if the caller sets a deadline that is shorter than the target API’s default deadline (i.e. requested deadline is not enough for the server to process the request), and the request did not finish within the deadline.

This error code is also assigned to local connection and read timeouts.

Most APIs can only result in a subset of the above error codes. In any case, you are not expected to explicitly handle all these error codes when implementing your error handlers. Most applications would only be interested in 1-2 specific error codes and treat everything else as a generic, unrecoverable failure.

Service-specific error codes

Firebase Auth

CERTIFICATE_FETCH_FAILED Failed to fetch public key certificates required to verify a JWT (ID token or session cookie).
EMAIL_ALREADY_EXISTS A user already exists with the provided email.
EXPIRED_ID_TOKEN The ID token specified to verifyIdToken() is expired.
EXPIRED_SESSION_COOKIE The session cookie specified to verifySessionCookie() iis expired.
INVALID_DYNAMIC_LINK_DOMAIN The provided dynamic link domain is not configured or authorized for the current project. Related to email action link APIs.
INVALID_ID_TOKEN The ID token specified to verifyIdToken() is invalid.
INVALID_SESSION_COOKIE The session cookie specified to verifySessionCookie() is invalid.
PHONE_NUMBER_ALREADY_EXISTS A user already exists with the provided phone number.
REVOKED_ID_TOKEN The ID token specified to verifyIdToken() is revoked.
REVOKED_SESSION_COOKIE The session cookie specified to verifySessionCookie() is expired.
UNAUTHORIZED_CONTINUE_URL The domain of the continue URL is not whitelisted. Related to email action link APIs.
USER_NOT_FOUND No user record found for the given identifier.

Firebase Cloud Messaging

THIRD_PARTY_AUTH_ERROR APNs certificate or web push auth API key was invalid or missing.
INVALID_ARGUMENT One or more arguments specified in the request were invalid.
INTERNAL Internal server error.
QUOTA_EXCEEDED Sending limit exceeded for the message target.
SENDER_ID_MISMATCH The authenticated sender ID is different from the sender ID for the registration token. This usually means the sender and the target registration token are not in the same Firebase project.
UNAVAILABLE Cloud Messaging service is temporarily unavailable.
UNREGISTERED App instance was unregistered from FCM. This usually means that the device registration token used is no longer valid and a new one must be used.

Automatic retries

The Admin SDK automatically retries certain errors before exposing those errors to the users. In general, following types of errors are transparently retried:

  • All API errors resulting from HTTP 503 (Service Unavailable) responses.
  • Some API errors resulting from HTTP 500 (Internal Server Error) responses.
  • Most low-level I/O errors (connection refused, connection reset etc).

The SDK will retry each of the above errors up to 5 times (the original attempt + 4 retries) with exponential backoff. You can implement your own retry mechanisms at the application level if you want, but this is typically not required.

Retry-After support

The Go and .NET implementations of the Admin SDK come with support for handling the HTTP Retry-After header. That is, if the error response sent by the backend servers contain the standard Retry-After header, the SDK will respect that when retrying as long as the specified wait duration is not very long. If the Retry-After header indicates a very long wait duration, the SDK will bypass retries and throw the appropriate API error.

The Python Admin SDK does not currently support the Retry-After header, and only supports simple exponential backoff.

API error handling examples

Implementing a generic error handler

In most cases what you want is a generic error handler that catches a broad range of errors to prevent unexpected termination of the program flow due to an API error. Such error handlers usually just log the errors for audit purposes, or invoke some other default error handling routine for all encountered API errors. They are not necessarily interested in the different error codes or the reasons that may have caused the error.

Java

try {
  FirebaseToken token = FirebaseAuth.getInstance().verifyIdToken(idToken);
  performPrivilegedOperation(token.getUid());
} catch (FirebaseAuthException ex) {
  System.err.println("Failed to verify ID token: " + ex.getMessage());
}

Python

try:
  token = auth.verify_id_token(idToken)
  perform_privileged_pperation(token.uid)
except exceptions.FirebaseError as ex:
  print(f'Failed to verify ID token: {ex}')

Go

token, err := client.VerifyIDToken(ctx, idToken)
if err != nil {
  log.Printf("Failed to verify ID token: %v", err)
  return
}

performPrivilegedOperation(token)

.Net

try
{
  var token = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
  PerformPrivilegedOperation(token.getUid());
}
catch (FirebaseAuthException ex) 
{
  Conole.WriteLine($"Failed to verify ID token: {ex.Message}");
}

Checking error codes

In some cases, you would want to inspect the exact error codes, and invoke different context-aware error handling routines. In the following example, we have an error handler that logs more specific error messages based on the service error code.

Java

try {
  FirebaseToken token = FirebaseAuth.getInstance().verifyIdToken(idToken);
  performPrivilegedOperation(token.getUid());
} catch (FirebaseAuthException ex) {
  if (ex.getAuthErrorCode() == AuthErrorCode.ID_TOKEN_EXPIRED) {
    System.err.println("ID token has expired");
  } else if (ex.getAuthErrorCode() == AuthErrorCode.ID_TOKEN_INVALID) {
    System.err.println("ID token is malformed or invalid");
  } else {
    System.err.println("Failed to verify ID token: " + ex.getMessage());
  }
}

Python

try:
  token = auth.verify_id_token(idToken)
  perform_privileged_operation(token.uid)
except auth.ExpiredIdTokenError:
  print('ID token has expired')
except auth.InvalidIdTokenError:
  print('ID token is malformed or invalid')
except exceptions.FirebaseError as ex:
  print(f'Failed to verify ID token: {ex}')

Go

token, err := client.VerifyIDToken(ctx, idToken)
if auth.IsIDTokenExpired(err) {
  log.Print("ID token has expired")
  return
}
if auth.IsIDTokenInvalid(err) {
  log.Print("ID token is malformed or invalid")
  return
}
if err != nil {
  log.Printf("Failed to verify ID token: %v", err)
  return
}

performPrivilegedOperation(token)

.Net

try
{
  var token = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
  PerformPrivilegedOperation(token.getUid());
}
catch (FirebaseAuthException ex)
{
  if (ex.AuthErrorCode == AuthErrorCode.ExpiredIdToken)
  {
    Console.WriteLine("ID token has expired");
  }
  else if (ex.AuthErrorCode == AuthErrorCode.InvalidIdToken)
  {
    Console.WriteLine("ID token is malformed or invalid");
  }
  else
  {
    Conole.WriteLine($"Failed to verify ID token: {ex.Message}");
  }
}

Here’s another example where we check for both top-level and service error codes:

Java

try {
  FirebaseMessaging.getInstance().send(createMyMessage());
} catch (FirebaseMessagingException ex){
  if (ex.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED) {
    System.err.println("App instance has been unregistered");
    removeTokenFromDatabase();
  } else if (ex.getErrorCode() == ErrorCode.Unavailable) {
    System.err.println("FCM service is temporarily unavailable");
    scheduleForRetryInAnHour();
  } else {
    System.err.println("Failed to send notification: " + ex.getMessage());
  }
}

Python

try:
  messaging.send(create_my_message())
except messaging.UnregisteredError:
  print('App instance has been unregistered')
  remove_token_from_database()
except exceptions.UnavailableError:
  print('FCM service is temporarily unavailable')
  schedule_for_retry_in_an_hour()
except exceptions.FirebaseError as ex:
  print(f'Failed to send notification: {ex}')

Go

_, err := client.Send(ctx, createMyMessage())
if messaging.IsUnregistered(err) {
  log.Print("App instance has been unregistered")
  removeTokenFromDatabase()
  return
}
if errorutils.IsUnavailable(err) {
  log.Print("FCM service is temporarily unavailable")
  scheduleForRetryInAnHour()
  return
}
if err != nil {
  log.Printf("Failed to send notification: %v", err)
  return
}

.Net

try
{
  await FirebaseMessaging.DefaultInstance.SendAsync(createMyMessage());
}
catch (FirebaseMessagingException ex)
{
  if (ex.MessagingErrorCode == MessagingErrorCode.UNREGISTERED)
  {
    Console.WriteLine("App instance has been unregistered");
    removeTokenFromDatabase();
  }
  else if (ex.ErrorCode == ErrorCode.Unavailable)
  {
    Console.WriteLine("FCM service is temporarily unavailable");
    scheduleForRetryInAnHour();
  }
  else
  {
    Console.WriteLine($"Failed to send notification: {ex.Message}");
  }
}

Accessing the HTTP response

In some rare cases you may want to inspect the HTTP error response returned by a backend service and perform some error handling action on it. The Admin SDK exposes both the headers and the contents of these error responses. The response content is usually returned as a string or a raw byte sequence, and can be parsed into any target format necessary.

Java

try {
  FirebaseMessaging.getInstance().send(createMyMessage());
} catch (FirebaseMessagingException ex){
  IncomingHttpResponse response = ex.getHttpResponse();
  if (response != null) {
    System.err.println("FCM service responded with HTTP " + response.getStatusCode());

    Map<String, Object> headers = response.getHeaders();
    for (Map.Entry<String, Object> entry : headers.entrySet()) {
      System.err.println(">>> " + entry.getKey() + ": " + entry.getValue());
    }

    System.err.println(">>>");
    System.err.println(">>> " + response.getContent());
  }
}

Python

try:
  messaging.send(create_my_message())
except exceptions.FirebaseError as ex:
  response = ex.http_response
  if response is not None:
    print(f'FCM service responded with HTTP {response.status_code}')

    for key, value in response.headers.items():
      print(f'>>> {key}: {value}')

    print('>>>')
    print(f'>>> {response.content}')

Go

_, err := client.Send(ctx, createMyMessage())
if resp := errorutils.HTTPResponse(err); resp != nil {
  log.Printf("FCM service responded with HTTP %d", resp.StatusCode)

  for key, value := range resp.Header {
      log.Printf(">>> %s: %v", key, value)
  }

  defer resp.Body.Close()
  b, _ := ioutil.ReadAll(resp.Body)
  log.Print(">>>")
  log.Printf(">>> %s", string(b))

  return
}

.Net

try
{
  await FirebaseMessaging.DefaultInstance.SendAsync(createMyMessage());
}
catch (FirebaseMessagingException ex)
{
  var response = ex.HttpResponse
  if response != null
  {
    Console.WriteLine($"FCM service responded with HTTP { response.StatusCode}");

    var headers = response.Headers;
    for (var entry in response.Headers)
    {
      Console.WriteLine($">>> {entry.Key}: {entry.Value}");
    }

    var body = await response.Content.ReadAsString();
    Console.WriteLine(">>>");
    Console.WriteLine($">>> {body}");
  }
}