react-native

Centralized Exception Handling in Supabase Edge Functions by Pranav Khandelwal

I have been building a full-stack mobile app with Supabase and wanted to centralize my exception handling into a single place.

I had a few goals with this implementation:

  1. I wanted a consistent response shape for all my errors

  2. I wanted to establish a clear pattern for error handling in my edge functions

  3. I wanted to centralize my error handling in one place

The general idea was that all my functions will throw exceptions and a global error handling middleware would capture and handle the exceptions as needed.

My implementation had 3 key components:

  1. An Error Response shape

  2. A CustomError class that extends Error

  3. A Global exception handling “middleware” to wrap all of my edge functions (taking inspiration from .NET)

I decided to model my Error Response like this:

export type ErrorResponse = {
    message: string;
    code: ErrorCode;
    metadata?: Record<string, string>;
};

export type ErrorCode =
    | "RELAY_ERROR"
    | "FETCH_ERROR"
    | "UNKNOWN_ERROR"
    | "UNAUTHORIZED"
    | "VALIDATION_ERROR"

All my API errors will return this in the body. The ErrorCode can be extended to add additional domain specific error codes that my app can use to conditionally execute logic, like rendering various messages or navigating to a specific route.

The metadata object can contain any additional information I wanted to pass back to the app.

Now that I had my shape defined, the next step was to implement my CustomError class:

class CustomError extends Error implements ErrorResponse {
    code: ErrorCode;
    status: number;
    metadata?: Record<string, string>;
    shouldLog?: boolean = false;
    constructor(
        error: string,
        code: ErrorCode,
        status: number,
        metadata?: Record<string, string>,
    ) {
        super(error);
        this.code = code;
        this.status = status;
        this.metadata = metadata;
    }

    toErrorResponse(): Response {
        const errorResponse: ErrorResponse = {
            message: this.message,
            code: this.code,
            metadata: this.metadata,
        };
        return new Response(JSON.stringify(errorResponse), {
            status: this.status,
            headers: {
                "Content-Type": "application/json",
            },
        });
    }

    toLogResponse(): string {
        return JSON.stringify(this.toErrorResponse(), null, 2);
    }
}

My CustomError class extends the base Error type. There are some helper functions to convert my CustomError to a Response object that can be returned from an edge function or a JSON string for logging.

I also added a shouldLog boolean to indicate to my middleware if the error should be logged to the console. Some errors, like validation errors, don’t need to be logged, so this flag helps avoid cluttering the logs with unnecessary lines.

The last step was to implement my middleware. The middleware was implemented as a higher-order-function that can wrap all my edge functions:

export const withGlobalErrorHandler = (
    handler: (req: Request) => Promise<Response>,
) => {
    return async (req: Request) => {
        try {
            return await handler(req);
        } catch (error) {
            logError(error);
            return error instanceof CustomError
                ? error.toErrorResponse()
                : new InternalServerError("Internal Server Error")
                    .toErrorResponse();
        }
    };
};

const logError = (error: unknown) => {
    if (error instanceof CustomError && error.shouldLog) 
        
     else if (error instanceof Error) 
        
     else {
        console.error(`Unknown error: $
    
};

Now, any exceptions thrown by my request handler will be captured here, logged if necessary, and handled appropriately. To use the middleware, I wrapped my edge functions like this:

Deno.serve(withGlobalErrorHandler(async (req) => {
  const  = await req.json() as { postId: string };
  
  const post = await getPost(postId);

  if (!post) {
    throw new CustomError("Post not found", "NOT_FOUND", 404);
  }

  return new Response(JSON.stringify(post), {
    headers: { "Content-Type": "application/json" },
  });
}));

Any exception thrown in the request handler will now be captured and handled by the middleware.

Closing Thoughts

There is a lot of room for improvement here. For instance, using exceptions for flow control is considered an anti-pattern. A better approach would be to use a Result type.

One obvious benefit of moving away from exceptions to something like a Result type is that the middleware can handle returning the API response for all possible outcomes: errors, unhandled exceptions, and successful results. Adding common headers, serializing the response, and other logic could then be centralized instead of duplicated across edge functions.

That being said, I like to be pragmatic instead of dogmatic. Since this is a hobby project, I just need something that is good enough to work for my use case. Software is built iteratively, so I can continue to improve the implementation overtime.

Changing App Splash Screen in Expo by Pranav Khandelwal

I really like Expo, but sometimes I find myself going down endless GitHub rabbit holes trying to fix simple things.

In this case, I changed the splash splash image in an Expo app I was working on and ran into a very annoying issue: the old splash image (which was deleted) continued to show briefly when the app was launched before suddenly changing to the new image.

According to Expo Docs - this can sometimes happen with SDK 51 and below because in iOS development builds, launch screens can sometimes remain cached between builds.

The problem was that I was not on SDK 51. I was on SDK 53. Either way, I tried the suggestion of running npx expo run:ios --no-build-cache to fix the issue…and it didn’t work. But it was worth a try.

After browsing through a bunch of GitHub threads, I noticed that a few others who experienced the same issue were able to resolve it by running npx expo prebuild.

The first time I ran this, I got a failure on Android: Could not find MIME for Buffer <null>.

This cryptic error went away if I commented out the “expo-splash-screen” plugin configuration in the app config. Strangely, once I commented it back in and ran prebuild again…the error didn’t happen and a different error emerged:

withIosSplashScreenStoryboardBaseMod: Cannot create property 'constraint' on string ''

More searching lead me to this comment: https://github.com/expo/expo/pull/32858#issuecomment-2476515215

Did you try running prebuild with --clean? If you don't, you may run into problems where it looks at your existing storyboard and tries to modify it but this has changed in the sdk 52 template.

Did i try running with - -clean? No, I did not. So I gave it a shot and it worked. No errors and all the issues went away. I saw the correct splash screen when launching the app on device.

Closing Thoughts:

I really like Expo and React Native. Cross platform frameworks allow me to build mobile apps for iOS and Android using a single codebase and when everything is working, the dev loop and iteration speed is best in class. It’s easy to get into flow and make progress quickly.

BUT…spending so much time debugging why a simple splash screen image change is not working really hurts the overall experience. These kinds of issues just don’t exist with pure native tools (XCode/Swift, Android Studio/Kotlin). Yes, I need to write and maintain two codebases, but if I change an image and re-run my app, the change just works. I hope the same experience can make it’s way into Expo too.




Connecting to Supabase running locally from Android Emulators by Pranav Khandelwal

While working on a mobile app using React Native and Expo, I ran into an issue where I was unable to connect to a local instance of Supabase from my Android Emulator.

My local Supabase instance was running at: http://127.0.0.1:54321.

When initializing the Supabase JavaScript client, I was simply passing in this URL and anon key:

const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey)

While this worked fine on iOS Simulator, on Android Emulators, I would keep getting an error:

TypeError: Network request failed

Per the official Android documentation, the reason why is because Android Emulators sit behind their own virtual router, so the virtual devices can't see the host computer. They only see a router connection. That router gives the emulator an IP in the 10.0.2.x range, keeping all traffic inside this private network.

The fix is to use a special alias IP that points to the host computer's own local address (127.0.0.1). Per the Android docs, this alias IP is:

10.0.2.2

Changing the URL to http://10.0.2.2:54321 fixed the issue on Android, but caused iOS to break with the same error: TypeError: Network request failed.

To fix the issue for both platforms, I used the Platform helper from react-native to conditionally select the correct URL:

export const supabaseUrl = Platform.select({
  ios: http://127.0.0.1:54321,
  android: http://10.0.2.2:54321,
});