spacer
Skip to content

Error Handling

This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns.

For 429, 503, and 504 errors we include retry logic within the library

The HttpRequestError

All errors resulting from executed web requests will be returned as an HttpRequestError object which extends the base Error. In addition to the standard Error properties it has some other properties to help you figure out what went wrong. We used a custom error to attempt to normalize what can be a wide assortment of http related errors, while also seeking to provide as much information to library consumers as possible.

Property Name Description
name Standard Error.name property. Always 'Error'
message Normalized string containing the status, status text, and the full response text
stack The callstack producing the error
isHttpRequestError Always true, allows you to reliably determine if you have an HttpRequestError instance
response Unread copy of the Response object resulting in the thrown error
status The Response.status value (such as 404)
statusText The Response.statusText value (such as 'Not Found')

Basic Handling

For all operations involving a web request you should account for the possibility they might fail. That failure might be transient or permanent - you won't know until they happen 😉. The most basic type of error handling involves a simple try-catch when using the async/await promises pattern.

import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists/web";

try {

  // get a list that doesn't exist
  const w = await sp.web.lists.getByTitle("no")();

} catch (e) {

  console.error(e);
}

This will produce output like:

Error making HttpClient request in queryable [404] Not Found ::> {"odata.error":{"code":"-1, System.ArgumentException","message":{"lang":"en-US","value":"List 'no' does not exist at site with URL 'https://tenant.sharepoint.com/sites/dev'."}}} Data: {"response":{"size":0,"timeout":0},"status":404,"statusText":"Not Found","isHttpRequestError":true}

This is very descriptive and provides full details as to what happened, but you might want to handle things a little more cleanly.

Reading the Response

In some cases the response body will have additional details such as a localized error messages which can be nicer to display rather than our normalized string. You can read the response directly and process it however you desire:

import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists/web";
import { HttpRequestError } from "@pnp/queryable";

try {

  // get a list that doesn't exist
  const w = await sp.web.lists.getByTitle("no")();

} catch (e) {

  // are we dealing with an HttpRequestError?
  if (e?.isHttpRequestError) {

    // we can read the json from the response
    const json = await (<HttpRequestError>e).response.json();

    // if we have a value property we can show it
    console.log(typeof json["odata.error"] === "object" ? json["odata.error"].message.value : e.message);

    // add of course you have access to the other properties and can make choices on how to act
    if ((<HttpRequestError>e).status === 404) {
       console.error((<HttpRequestError>e).statusText);
      // maybe create the resource, or redirect, or fallback to a secondary data source
      // just ideas, handle any of the status codes uniquely as needed
    }

  } else {
    // not an HttpRequestError so we just log message
    console.log(e.message);
  }
}

Logging errors

Using the PnPjs Logging Framework you can directly pass the error object and the normalized message will be logged. These techniques can be applied to any logging framework.

import { Logger } from "@pnp/logging";
import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists/web";

try {
  // get a list that doesn't exist
  const w = await sp.web.lists.getByTitle("no")();  
} catch (e) {

  Logger.error(e);
}

You may want to read the response and customize the message as described above:

import { Logger } from "@pnp/logging";
import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists/web";
import { HttpRequestError } from "@pnp/queryable";

try {
  // get a list that doesn't exist
  const w = await sp.web.lists.getByTitle("no")();  
} catch (e) {

  if (e?.isHttpRequestError) {

    // we can read the json from the response
    const data = await (<HttpRequestError>e).response.json();

    // parse this however you want
    const message = typeof data["odata.error"] === "object" ? data["odata.error"].message.value : e.message;

    // we use the status to determine a custom logging level
    const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;

    // create a custom log entry
    Logger.log({
      data,
      level,
      message,
    });

  } else {
    // not an HttpRequestError so we just log message
    Logger.error(e);
  }
}

Putting it All Together

After reviewing the above section you might have thought it seems like a lot of work to include all that logic for every error. One approach is to establish a single function you use application wide to process errors. This allows all the error handling logic to be easily updated and consistent across the application.

errorhandler.ts

import { Logger } from "@pnp/logging";
import { HttpRequestError } from "@pnp/queryable";
import { hOP } from "@pnp/core";

export async function handleError(e: Error | HttpRequestError): Promise<void> {

  if (hOP(e, "isHttpRequestError")) {

    // we can read the json from the response
    const data = await (<HttpRequestError>e).response.json();

    // parse this however you want
    const message = typeof data["odata.error"] === "object" ? data["odata.error"].message.value : e.message;

    // we use the status to determine a custom logging level
    const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;

    // create a custom log entry
    Logger.log({
      data,
      level,
      message,
    });

  } else {
    // not an HttpRequestError so we just log message
    Logger.error(e);
  }
}

web-request.ts

import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists/web";
import { handleError } from "./errorhandler";

try {

  const w = await sp.web.lists.getByTitle("no")();

} catch (e) {

  await handleError(e);
}

web-request2.ts

import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists/web";
import { handleError } from "./errorhandler";

try {

  const w = await sp.web.lists();

} catch (e) {

  await handleError(e);
}

Building a Custom Error Handler

In Version 3 the library introduced the concept of a Timeline object and moments. One of the broadcast moments is error. To create your own custom error handler you can define a special handler for the error moment something like the following:


//Custom Error Behavior
export function CustomError(): TimelinePipe<Queryable> {

    return (instance: Queryable) => {

        instance.on.error((err) => {
            if (logging) {
                console.log(`🛑 PnPjs Testing Error - ${err.toString()}`);
            }
        });

        return instance;
    };
}

//Adding our CustomError behavior to our timline

const sp = spfi().using(SPDefault(this.context)).using(CustomError());