Handling Flow Errors

This guide will cover displaying and controlling behavior around flow errors. Within Descope screens, if an external error is provided during an action, you can transform the error to be a more consumable error for your end user. Outside of screens, you can route based on errors.

Customizing Flow Errors via Client SDKs

Install SDK

ReactNextJSNextAuthWebJSVue.jsHTMLAngular
npm i --save @descope/react-sdk
npm i --save @descope/next-sdk
npm i --save next-auth
npm i --save @descope/web-js-sdk
npm i --save @descope/vue-sdk
<!-- Not applicable for HTML -->
npm i --save @descope/angular-sdk

Import and initialize SDK

ReactNextJSNextAuthWebJSVue.jsHTMLAngular
import { AuthProvider } from '@descope/react-sdk'
import { Descope, useDescope } from '@descope/react-sdk'

const AppRoot = () => {
	return (
		<AuthProvider
			projectId="__ProjectID__"
			// If the Descope project manages the token response in cookies,
            // a custom domain must be configured
            // (e.g., https://auth.app.example.com)
			// and should be set as the baseUrl property.
			// baseUrl = "https://auth.app.example.com"
		>
			<App />
		</AuthProvider>
	);
};
import { AuthProvider } from '@descope/nextjs-sdk';

export default function RootLayout({ children }: { children: React.ReactNode }) {
	return (
		<AuthProvider projectId="__ProjectID__">
			<html lang="en">
				<body>{children}</body>
			</html>
		</AuthProvider>
	);
}
// app/api/auth/[...nextauth]
import { NextAuthOptions } from "next-auth"
import NextAuth from "next-auth/next";

export const authOptions: NextAuthOptions = {
  providers: [
    {
      id: "descope",
      name: "Descope",
      type: "oauth",
      wellKnown: `https://api.descope.com/${process.env.DESCOPE_PROJECT_ID}/.well-known/openid-configuration`,
      authorization: { params: { scope: "openid email profile" } },
      idToken: true,
      clientId: process.env.DESCOPE_PROJECT_ID,
      clientSecret: process.env.DESCOPE_ACCESS_KEY,
      checks: ["pkce", "state"],
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.name,
          email: profile.email,
          image: profile.picture,
        }
      },
    },
  ], 
  callbacks: {
    async jwt({token, account, profile}) {
      if (account) {
        return {
          ...token,
          access_token: account.access_token,
          expires_at: Math.floor(Date.now() / 1000 + account.expires_in),
          refresh_token: account.refresh_token,
          profile: {
            name: profile?.name,
            email: profile?.email,
            image: profile?.picture,
            },
          }
        } else if (Date.now() < token.expires_at * 1000) {
          return token
        } else {
          try {
            const response = await fetch("https://api.descope.com/oauth2/v1/token", {
              headers: {"Content-Type": "application/x-www-form-urlencoded"},
              body: new URLSearchParams({
                client_id: "__ProjectID__",
                client_secret: "<Descope Access Key>",
                grant_type: "refresh_token",
                refresh_token: token.refresh_token,
              }),
              method: "POST",
            })
            
            const tokens = await response.json()
            
            if (!response.ok) throw tokens
            
            return {
              ...token,
              access_token: tokens.access_token,
              expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in),
              refresh_token: tokens.refresh_token ?? token.refresh_token,
            }
          } catch (error) {
            console.error("Error refreshing access token", error)
            return {...token, error: "RefreshAccessTokenError"}
            }
          }
        },
    
        async session({session, token}) {
          if (token.profile) {
            session.user = token.profile;
          }
    
          session.error = token.error
          session.accessToken = token.access_token
          return session
        },
    }
}

const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

// app/provider.tsx
'use client'

import { SessionProvider } from "next-auth/react"


export default function NextAuthSessionProvider(
    { children }:
    { children: React.ReactNode }
) {
    return (
        <SessionProvider>
            { children }
        </SessionProvider>
    )
}

// app/layout.tsx
import NextAuthSessionProvider from './provider'


export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <NextAuthSessionProvider>
          <div>
            {children}
          </div>
        </NextAuthSessionProvider>
      </body>
    </html>
  )
}
import DescopeSdk from '@descope/web-js-sdk';
try {
    //  baseUrl="<URL>" // you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize CNAME within your Descope project.
    const descopeSdk = DescopeSdk({ projectId: '__ProjectID__', persistTokens: true, autoRefresh: true });
} catch (error) {
    // handle the error
    console.log("failed to initialize: " + error)
}
import { createApp } from "vue";
import App from "@/App.vue";
import descope, { getSdk } from "@descope/vue-sdk";

const app = createApp(App);
app.use(router);

app.use(descope, {
  projectId: '__Project_ID__',
  baseUrl: "<base url>", // Optional
});

const sdk = getSdk();
sdk?.onSessionTokenChange((newSession) => {
  // here you can implement custom logic when the session is changing
});
<head>
    <script src="https://unpkg.com/@descope/web-js-sdk@x.x.x/dist/index.umd.js"></script>
    <!-- Please replace `x.x.x` with the latest version of the WebJS SDK, which you can get from [here](https://www.npmjs.com/package/@descope/web-js-sdk) -->
</head>
// app.module.ts
import { NgModule, APP_INITIALIZER} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { DescopeAuthModule, DescopeAuthService, descopeInterceptor } from '@descope/angular-sdk';
import { AppComponent } from './app.component';
import {
	HttpClientModule,
	provideHttpClient,
	withInterceptors
} from '@angular/common/http';
import { zip } from 'rxjs';

export function initializeApp(authService: DescopeAuthService) {
	return () => zip([authService.refreshSession(), authService.refreshUser()]);
}

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    DescopeAuthModule.forRoot({
      projectId: 'YOUR_PROJECT_ID'
    })
  ],
  providers: [
    {
        provide: APP_INITIALIZER,
        useFactory: initializeApp,
        deps: [DescopeAuthService],
        multi: true
      },
      provideHttpClient(withInterceptors([descopeInterceptor]))
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Customizing Flow Errors

Below are examples of how to transform errors during flow execution.

ReactNextJSWebJSVue.jsHTML
import { Descope } from '@descope/react-sdk'

const App = () => {
  return (
    {...}
    <Descope
      flowId="my-flow-id"
      onSuccess={(e) => console.log('Logged in!')}
      onError={(e) => console.log('Could not logged in')}

      const errorTransformer = useCallback(
      	(error: { text: string; type: string }) => {
      		const translationMap = {
      			SAMLStartFailed: 'Failed to start SAML flow'
      		};
      		return translationMap[error.type] || error.text;
      	},
      	[]
      );
      errorTransformer={errorTransformer}
    />
  )
}
export function translateError(error) {
  const translationMap = {
    SAMLStartFailed:
      'Failed to start SAML flow',
  };
  return translationMap[error.type] || error.text;
}
function translateError(error) {
  const translationMap = {
    SAMLStartFailed:
      'Failed to start SAML flow',
  };
  return translationMap[error.type] || error.text;
}

const descopeWcEle = document.getElementsByTagName('descope-wc')[0];

descopeWcEle.errorTransformer = translateError;
<template>
  <div class="wrapper">
    <p v-if="isLoading">Loading...</p>
    <div v-else-if="isAuthenticated">
      <h1>You are authenticated</h1>
    </div>
    <Descope
      v-else
      :flowId="sign-up-or-in" // can be any flow id
      @error="handleError"
      @success="handleSuccess"
      :errorTransformer="errorTransformer"
    />
  </div>
</template>

<script setup>
import { Descope, useSession } from "@descope/vue-sdk";
const { isLoading, isAuthenticated } = useSession();

const handleError = (e) => {
  console.log("Got error", e);
};

const handleSuccess = (e) => {
  console.log("Logged in", e);
};

const errorTransformer = (error) => {
  const translationMap = {
    SAMLStartFailed: "Failed to start SAML flow",
  };
  return translationMap[error.type] || error.text;
};
</script>
<script>
function translateError(error) {
  const translationMap = {
    SAMLStartFailed:
      'Failed to start SAML flow',
  };
  return translationMap[error.type] || error.text;
}

const descopeWcEle = document.getElementsByTagName('descope-wc')[0];

descopeWcEle.errorTransformer = translateError;
</script>

Customizable Errors

Below outlines the customizable errors you can transform when running Descope flows.

CategoryErrors
OTP
  • OTPSignUpOrInEmailFailed
  • OTPSignUpOrInPhoneFailed
  • OTPVerifyCodeEmailFailed
  • OTPVerifyCodePhoneFailed
  • OTPUpdateUserEmailFailed
  • OTPUpdateUserPhoneFailed
  • OTPUnauthorizedRequest
  • OTPSignInEmbeddedFailed
  • OTPSignUpOrInEmbeddedFailed
  • OTPSignUpEmbeddedFailed
  • OTPVerifyEmbeddedFailed
TOTP
  • TOTPSignUpFailed
  • TOTPVerifyCodeFailed
  • TOTPUpdateUserFailed
  • TOTPUnauthorizedRequest
Magic Link
  • MagicLinkSignUpOrInFailed
  • MagicLinkSignUpOrInFailed
  • MagicLinkMisconfiguration
  • MagicLinkUpdateUserEmailFailed
  • MagicLinkUpdateUserPhoneFailed
  • MagicLinkUnauthorizedRequest
  • MagicLinkVerifyFailed
Enchanted Link
  • EnchantedLinkSignUpOrInFailed
  • EnchantedLinkMisconfiguration
  • EnchantedLinkUpdateUserEmailFailed
  • EnchantedLinkUnauthorizedRequest
  • EnchantedLinkVerifyFailed
Social (OAuth)
  • OAuthStartFailed
  • OAuthMisconfiguration
  • OAuthExchangeCodeFailed
Biometrics (WebAuthn)
  • WebauthnFailed
  • WebauthnSignUpStartFailed
  • WebauthnSignUpFinishFailed
  • WebauthnSignInStartFailed
  • WebauthnSignInFinishFailed
  • WebauthnSignUpOrInStartFailed
  • WebauthnSignUpOrInFinishFailed
  • WebauthnUpdateUserStartFailed
  • WebauthnUnauthorizedRequest
  • WebauthnUpdateUserFinishFailed
SAML Login
  • SAMLStartFailed
  • SAMLMisconfiguration
  • SAMLExchangeCodeFailed
Passwords
  • PasswordSignUpFailed
  • PasswordSignInFailed
  • PasswordExpired
  • PasswordSendResetFailed
  • PasswordUpdateFailed
  • PasswordReplaceFailed
  • ActionErrorPasswordUpdate
Tenants & SSO Config
  • TenantCreation
  • SetSAMLConfigFailed
  • GetSAMLConfigFailed
  • SAMLConfigSelectTenant
  • ConfigProvideTenant
  • GetTenantConfigFailed
  • SetTenantConfigFailed
User Invites
  • InviteUsersSelectTenant
  • InviteUsersPermissions
  • InviteUsers
Roles
  • GetDefaultRoles
  • AssignRoles
  • ActionErrorAddRolesFailed
  • ActionErrorAddRolesFailedDoNotExists
Connectors
  • ConnectorFailed
  • EmailConnectorFailed
  • SMSConnectorFailed
User Impersonation
  • ImpersonationConsent
  • ActionErrorImpersonateFailed
  • ActionErrorImpersonateNoPermission
  • ActionErrorImpersonateConsent
  • ActionErrorMissingImpersonatingUser
General
  • InvalidJWT
  • LoggedInFailed
  • ActionErrorLoadUserFromJWTFailed
  • ActionErrorLoadUserJwtVerify
  • ActionErrorLoadUserNoUser
  • ActionErrorParseCustomClaims
  • ActionErrorUpdateJWT
  • UpdateUserPropertiesFailed

Drag and Drop Flow Error Handling

Error handling in flows is done in a couple ways:

  • (1) Auto-handling: If a widget is not linked to something, the error is automatically presented in the most recent flow screen step.
  • (2) Specifically Exposed Errors: Certain widgets have hard-coded error handling, allowing the Descoper to configure the output.
  • (3) Custom Error Handling: Errors thrown in widgets that are not caught automatically are exposed as generic, allowing the Descoper to route to a desired screen or action.
Descope error handling config

By adjusting the Error Handling in the configuration of a widget from Automatic to Custom, the ability to route to a particular action, screen, condition, or connector will show up.

Descope error handling widget