- Published on
Error Handling Patterns for PnPjs in Production SPFx
Most SPFx tutorials wrap every PnPjs call in a try/catch that logs to the console and moves on. That is fine for a demo. In production, users need a clear message, errors need to be logged somewhere useful, and the web part needs to recover gracefully without leaving the page in a broken state.
This article covers the error handling patterns that make the difference between a demo and a production-grade SPFx solution.
πΊοΈ What PnPjs Errors Look Like
PnPjs v3 throws Error objects with additional properties attached. Understanding the shape lets you write specific, actionable error handlers instead of generic catch-all blocks.
try {
await sp.web.lists.getByTitle('NonExistentList').items();
} catch (err) {
console.log(err.message); // Human-readable message
console.log(err.status); // HTTP status code (404, 403, 500 etc.)
console.log(err.response); // The raw fetch Response object
console.log(err.data); // Parsed response body (if JSON)
}
Common status codes you will encounter:
| Status | Meaning | Common cause in SPFx |
|---|---|---|
| 400 | Bad Request | Malformed query, invalid field name, wrong wire format |
| 403 | Forbidden | User lacks permission on the list or site |
| 404 | Not Found | List, item, or site does not exist |
| 429 | Too Many Requests | SharePoint throttling |
| 500 | Server Error | Corrupt list data, tenant configuration issue |
| 503 | Service Unavailable | SharePoint maintenance or transient failure |
π§© Pattern 1 β Typed Error Extraction
A utility function that extracts a typed error object from any PnPjs throw β useful as a single normalisation point across your entire service layer:
export type SpErrorCode = 'NOT_FOUND' | 'FORBIDDEN' | 'BAD_REQUEST' | 'THROTTLED' | 'SERVER_ERROR' | 'UNKNOWN';
export interface ISpError {
code: SpErrorCode;
message: string;
status: number | null;
raw: unknown;
}
export function extractSpError(err: unknown): ISpError {
if (err instanceof Error) {
const status = (err as any).status as number | undefined;
const codeMap: Record<number, SpErrorCode> = {
400: 'BAD_REQUEST',
403: 'FORBIDDEN',
404: 'NOT_FOUND',
429: 'THROTTLED',
500: 'SERVER_ERROR',
503: 'SERVER_ERROR'
};
// Try to extract a detailed message from the SharePoint error body
let message = err.message;
try {
const body = (err as any).data;
if (body?.['odata.error']?.message?.value) {
message = body['odata.error'].message.value;
}
} catch {
// Use the default message
}
return {
code: status ? (codeMap[status] ?? 'UNKNOWN') : 'UNKNOWN',
message,
status: status ?? null,
raw: err
};
}
return { code: 'UNKNOWN', message: 'An unknown error occurred', status: null, raw: err };
}
π§© Pattern 2 β Result Type β No Exceptions in Service Methods
Throwing exceptions across service boundaries makes error handling the caller's problem. A Result<T> return type makes errors explicit in the type system β callers cannot forget to handle them:
export type Result<T> =
| { ok: true; data: T }
| { ok: false; error: ISpError };
export class ListService {
private readonly _sp: SPFI;
constructor(sp: SPFI) { this._sp = sp; }
public async getAnnouncements(): Promise<Result<IAnnouncementItem[]>> {
try {
const items = await this._sp.web.lists
.getByTitle('Announcements')
.items
.select('Id', 'Title', 'Body', 'Expires')
.top(10)();
return { ok: true, data: items };
} catch (err) {
return { ok: false, error: extractSpError(err) };
}
}
}
Consuming the result β the caller is forced by the type system to handle both cases:
const result = await listService.getAnnouncements();
if (!result.ok) {
if (result.error.code === 'NOT_FOUND') {
setError('The Announcements list does not exist on this site.');
} else if (result.error.code === 'FORBIDDEN') {
setError('You do not have permission to view announcements.');
} else {
setError('Failed to load announcements. Please try again.');
}
return;
}
setItems(result.data);
π§© Pattern 3 β User-Facing Error Messages
Never show raw SharePoint error messages to end users. They contain internal paths, GUIDs, and technical jargon that is confusing and potentially exposes information. Map error codes to friendly messages:
export function getUserFacingMessage(error: ISpError): string {
switch (error.code) {
case 'NOT_FOUND':
return 'The requested content could not be found. Please contact your administrator.';
case 'FORBIDDEN':
return 'You do not have permission to access this content.';
case 'THROTTLED':
return 'SharePoint is temporarily busy. Please wait a moment and refresh.';
case 'BAD_REQUEST':
return 'There was a problem with the request. Please contact support.';
case 'SERVER_ERROR':
return 'SharePoint encountered an error. Please try again later.';
default:
return 'An unexpected error occurred. Please refresh the page.';
}
}
In your React component:
{error && (
<MessageBar intent="error">
{getUserFacingMessage(error)}
</MessageBar>
)}
π§© Pattern 4 β Logging Errors for Debugging
User-facing messages are sanitised, but you still need the full error details for debugging. Log them to the browser console in development and to an Application Insights instance in production:
import { Logger, LogLevel } from '@pnp/logging';
export function logSpError(context: string, error: ISpError): void {
const message = `[${context}] ${error.code} (${error.status}): ${error.message}`;
// Always log to console in dev
Logger.write(message, LogLevel.Error);
// In production, send to Application Insights if available
if ((window as any).appInsights) {
(window as any).appInsights.trackException({
exception: new Error(message),
properties: { context, errorCode: error.code, status: error.status }
});
}
}
Usage:
const result = await listService.getAnnouncements();
if (!result.ok) {
logSpError('AnnouncementsWebPart.loadData', result.error);
setErrorMessage(getUserFacingMessage(result.error));
}
π§© Pattern 5 β React Error Boundary for Catastrophic Failures
PnPjs errors inside useEffect are caught by your try/catch. But synchronous errors during render β from unexpected null data, missing properties, or broken component state β bypass useEffect entirely and crash the React tree.
A React error boundary catches these and shows a fallback instead of a blank broken page:
import * as React from 'react';
import { MessageBar } from '@fluentui/react-components';
interface IErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class WebPartErrorBoundary extends React.Component<
React.PropsWithChildren<{}>,
IErrorBoundaryState
> {
constructor(props: React.PropsWithChildren<{}>) {
super(props);
this.state = { hasError: false, error: null };
}
public static getDerivedStateFromError(error: Error): IErrorBoundaryState {
return { hasError: true, error };
}
public componentDidCatch(error: Error, info: React.ErrorInfo): void {
console.error('WebPart render error:', error, info.componentStack);
}
public render(): React.ReactNode {
if (this.state.hasError) {
return (
<MessageBar intent="error">
This web part encountered an error and could not be displayed.
Please refresh the page or contact support.
</MessageBar>
);
}
return this.props.children;
}
}
Wrap your root component in the boundary inside the web part's render:
public render(): void {
ReactDOM.render(
React.createElement(FluentProvider, { theme: webLightTheme },
React.createElement(WebPartErrorBoundary, null,
React.createElement(MyComponent, { sp: this._sp })
)
),
this.domElement
);
}
β Production Error Handling Checklist
Before shipping any SPFx web part to production, verify:
- Every PnPjs call is wrapped in
try/catchor uses theResult<T>pattern - Error codes are mapped to user-friendly messages β no raw SharePoint error text shown to users
- 403 errors show a clear permissions message, not a generic "something went wrong"
- 404 errors degrade gracefully β lists that might not exist on all sites are handled
- 429 throttling errors prompt the user to wait and retry, not just silently fail
- All errors are logged with enough context to diagnose in production (context name, error code, status)
- A React error boundary wraps the root component to catch render errors
- The web part never shows a blank screen β there is always a loading state, error state, or empty state
β Summary
- Extract a typed
ISpErrorfrom every PnPjs throw β you need the status code to give a specific response. - Use a
Result<T>return type in service methods β forces callers to handle errors explicitly. - Map error codes to user-friendly messages β never show raw SharePoint errors to end users.
- Log the full error details for debugging while showing the sanitised message to users.
- Wrap the root component in a React error boundary to handle catastrophic render errors gracefully.
- The 5-item checklist above is the minimum bar for production readiness.
Happy coding!
Author
Ravichandran@Hi_Ravichandran
