Errors and Troubleshooting

Error Handling in SDKs

Every SDK call that reaches the Descope servers can fail. When it does, the SDK reports a Descope error code (for example E061102), a human-readable description, and sometimes a more specific message. You can look up any code on the Common Errors page.

Note

For a list of common error codes and what they mean, see the Common Errors reference.

Checking for Errors by SDK

Each tab shows how one SDK types its errors and which codes it exposes, plus how to:

  • Check for a specific error code
  • Check for a category, such as unauthorized or rate limited
  • Read the code and description off an error

The Node.js SDK never throws on a failed request. Every call returns an SdkResponse<T> — check ok before using data, and read the details off error.

SdkResponse shape
{
  ok: boolean;            // false when the request failed
  code?: number;          // HTTP status code (e.g. 401, 429)
  error?: {
    errorCode: string;        // Descope error code, e.g. "E061102"
    errorDescription: string; // human-readable summary
    errorMessage?: string;    // extra context (not always present)
  };
  data?: T;               // present when ok === true
}

Import the client and the typed error-code map (DescopeErrors):

error-handling.js
import DescopeClient from '@descope/node-sdk';
const descopeClient = DescopeClient({ projectId: '__ProjectID__' });
const { DescopeErrors } = DescopeClient;

const resp = await descopeClient.otp.verify.email(loginId, code);
if (!resp.ok) {
  // Read the code + description off the response
  console.log(resp.error?.errorCode);         // "E061103"
  console.log(resp.error?.errorDescription);
  console.log(resp.code);                      // HTTP status, e.g. 401

  // Check for a specific typed error code
  if (resp.error?.errorCode === DescopeErrors.tooManyOTPAttempts) {
    // too many wrong OTP attempts (E061103)
  }

  // Check for a category using the HTTP status code
  if (resp.code === 429) {
    // rate limited
  }
} else {
  // Success — use resp.data
  const { sessionJwt } = resp.data;
}

Codes exposed on DescopeErrors:

ConstantCode
DescopeErrors.badRequestE011001
DescopeErrors.missingArgumentsE011002
DescopeErrors.invalidRequestE011003
DescopeErrors.invalidArgumentsE011004
DescopeErrors.wrongOTPCodeE061102
DescopeErrors.tooManyOTPAttemptsE061103
DescopeErrors.enchantedLinkPendingE062503
DescopeErrors.userNotFoundE062108

The Go SDK returns a standard error whose concrete type is *descope.Error. Use the package helpers to inspect it.

descope.Error shape
type Error struct {
    Code        string         // e.g. "E061102"
    Description string         // human-readable summary
    Message     string         // extra context (not always present)
    Info        map[string]any // extra metadata (HTTP status, Retry-After)
}
error_handling.go
result, err := descopeClient.Auth.OTP().VerifyCode(ctx, descope.MethodEmail, loginID, code, w)

// 1. Check for a specific error code
if descope.IsError(err, "E061102") {
    // wrong OTP code
}

// 2. Or match a predefined error with the standard errors.Is (requires: import "errors")
if errors.Is(err, descope.ErrInvalidOneTimeCode) {
    // wrong OTP code (E061102)
}

// 3. Check for a category (400 / 401 / 403 / 404)
if descope.IsUnauthorizedError(err) {
    // unauthorized
}

// 4. Read the code and description directly
if de := descope.AsError(err); de != nil {
    log.Printf("failed: [%s] %s", de.Code, de.Description)
}

Rate-limit errors carry a Retry-After value in the Info map:

if de := descope.AsError(err, descope.ErrRateLimitExceeded.Code); de != nil {
    retryAfter := de.Info[descope.ErrorInfoKeys.RateLimitExceededRetryAfter] // seconds
}

Predefined error variables (each is a *descope.Error with a fixed Code):

VariableCode
descope.ErrBadRequestE011001
descope.ErrMissingArgumentsE011002
descope.ErrValidationFailureE011003
descope.ErrInvalidArgumentsE011004
descope.ErrInvalidOneTimeCodeE061102
descope.ErrUserAlreadyExistsE062107
descope.ErrEnchantedLinkUnauthorizedE062503
descope.ErrPasswordExpiredE062909
descope.ErrTokenExpiredByLoggedOutE064001
descope.ErrNOTPUnauthorizedE066103
descope.ErrManagementUserNotFoundE112102
descope.ErrRateLimitExceededE130429

Client-side errors (SDK/config, not from the server) use G-prefixed codes, such as descope.ErrMissingProjectID (G010001), descope.ErrInvalidToken (G030002), and descope.ErrRefreshToken (G030003).

The .NET SDK throws DescopeException when a request fails. It exposes ErrorCode, ErrorDescription, and ErrorMessage, and its Message is formatted as [code]: description (message).

DescopeException shape
public class DescopeException : ApplicationException
{
    public string? ErrorCode { get; }        // "E062504"
    public string? ErrorDescription { get; } // "Token expired"
    public string? ErrorMessage { get; }     // "Failed to load magic link token"
    // Message => "[E062504]: Token expired (Failed to load magic link token)"
}
ErrorHandling.cs
using Descope;

try
{
    var response = await client.Auth.V1.Auth.Magiclink.Verify.PostAsync(
        new VerifyMagicLinkRequest { Token = token });
}
catch (DescopeException e)
{
    // Read the details off the exception
    Console.WriteLine(e.ErrorCode);        // "E062504"
    Console.WriteLine(e.ErrorDescription); // "Token expired"
    Console.WriteLine(e.Message);          // "[E062504]: Token expired (Failed to load magic link token)"

    // Check for a specific error code
    if (e.ErrorCode == "E061103")
    {
        // too many wrong OTP attempts
    }
}

As an example, the SDK automatically retries server errors (HTTP 503, 520, 521, 522, 524, 530) before raising. If a non-success response can't be parsed as a Descope error, the exception falls back to a synthetic code of HTTP<status> - for example HTTP401 for an unauthorized response:

catch (DescopeException e)
{
    if (e.ErrorCode == "HTTP401")
    {
        // unauthorized (server returned 401 with no parseable error body)
    }
}
Was this helpful?

On this page