0

I've already read tons of resources to try to help me on this. This gist did not solve it for me (https://github.com/github/fetch/issues/203#issuecomment-266034180). It also seemed like this (JavaScript Promises - reject vs. throw) would be my answer but it is not. Also this (Error thrown in awaited Promise not caught in catch block) and this (errors not being thrown after promise).

I'm developing a project using a Yii2 PHP server-side solution, and Vue frontend solution. The project has several resources (lessons, media, etc) and REST API endpoints on the server-side that all are used the same. My dev work would benefit from me creating a re-usable API client class (in native JS - not anyting Vue related). I created an 'abstract' class that I 'extend' for each resource and use its functions for the CRUD operations.

I'd like to set up some middleware functions that are going to process the response from the API so that will be handled in the same fashion after every request I make so that I don't have to reproduce that processing code in the Vue apps and components that are using those API client classes.

The code is using the native JS fetch() function. I'm using .then() and .catch() in the functions as needed to process responses and control the flow.

My problem is that I have a function to process the API response, and in it I throw an error if I receive a non-200 response. I've implemented .catch() blocks in several places but I always get an error "Uncaught (in promise)" regardless of putting catch() calls everywhere.

When a user starts watching a video, I make an API call to my server to update a status on a user_media record. So, in the Vue component, I use my UserMedia helper class to create() a resource on the server and implement a then() and catch() on that. When there is an error server-side, I expect the catch() to catch that error and handle it. But, I just get the error "Uncaught (in promise)" as if I'm not trying to catch the error at all.

In the code, I am using updateWatchedStatus() in the vimeo video component, that calls the UserMediaApi.create() which calls YiiApiHelper.request() which calls YiiApiHelper.processRestResponse() where the error is thrown. I've tried implementing catch() blocks all over the place but it's never caught.

CLEARLY, I don't understand something about either fetch(), promises, or catching errors. But I can't figure it out. It seems like the only way around this is to have to write a bunch more code to try to compensate. Any help is appreciated. Even if I'm going about this all wrong and should be doing it someway else entirely.

The full code for that can be seen here:

YiiApiHelper.js https://pastebin.com/HJNWYQXg
UserMediaApi.js https://pastebin.com/9u8jkcSP
Vimeo Video Vue Component https://pastebin.com/4dJ1TtdM

For brevity, here's what's important:

Generic API Helper:

const request = function(resource, options){
    return fetch(resource, options)
        .then(response => Promise.all([response, response.json()]));
}
const resourceUrl = function(){
    return this.autoPluralizeResource ?
        this.resourceName+'s' :
        this.resourceName;
}
const create = function(postData, options){
    const url = new URL(this.baseUrl+'/'+this.resourceUrl());
    if(!options){
        options = {};
    }
    options = {
        method: 'POST',
        body: JSON.stringify(postData),
        ...options,
    }

    if(!options.headers){
        options.headers = {};
    }
    options.headers = {
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
        "Content-Type": "application/json",
        ...options.headers
    }
    return this.request(url, options)
        .then(this.processRestResponse);
}
const processRestResponse = function([response, body]){
    if(!response.ok){
        if(response.status ==  422){
            if(Array.isArray(body)){
                let messages = [];
                body.forEach(validationError => {
                    messages.push(validationError.message);
                })
                throw {
                    name: response.status,
                    message: messages.join("\n")
                }
            }
        }
        throw {
            name: response.status,
            message: (body.message) ?
                body.message :
                response.statusText
        }
    }
    return Promise.all([response, body]);
}
export default {
    baseUrl: '',
    resourceName: '',
    autoPluralizeResource: true,

    resourceUrl: resourceUrl,
    request: request,
    create: create,

    processRestResponse: processRestResponse,
    handleErrorResponse: handleErrorResponse
};

UserMedia helper:

import YiiApiHelper from './../../yiivue/YiiApiHelper.js';
export default {
    ...YiiApiHelper,
    baseUrl: window.location.origin+'/media/api/v1',
    resourceName: 'user-media',
    autoPluralizeResource: false
}

VimeoVideo.js:

let updateWatchedStatus = function(watchedStatusId) {
    if(!props.userMedia){
        // --- User has no record for this media, create one
        return UserMediaApi.create({
                media_id: props.media.id,
                user_id: props.userId,
                data: {
                    [Helper.WATCHED_STATUS_KEY]: watchedStatusId
                }
            }).then(([response, body])  => {
                context.emit('userMediaUpdated', {userMedia: body});
                return body;
            }).catch(YiiApiHelper.handleErrorResponse);;
    }

    // --- User has a record, update the watched status in the data
    let data = {
        ...userMedia.value.data,
        [Helper.WATCHED_STATUS_KEY]: watchedStatusId
    }
    return UserMediaApi.update(props.media.id+','+props.userId, {
            data: data
        }).then(([response, body])  => {
            context.emit('userMediaUpdated', {userMedia: body});
            return body;
        }).catch(YiiApiHelper.handleErrorResponse);;
}
BVBAccelerate
  • 172
  • 1
  • 5
  • 17
  • 2
    Can you try to cut your question and code down to the absolute minimum that explains and demonstrates the issue? – Evert Nov 05 '22 at 04:50
  • @Evert I don't know how to do this without writing something completely new that reproduces the same issue. And, I'm honestly not sure how to do that because if I did I'd probably be able to figure out my issue here too. – BVBAccelerate Nov 06 '22 at 21:44
  • to answer your question, it requires the same work (stripping down the code until the issue is clear). I would recommend you don't see this site as a way to do pair programming or help you debug. My advice is keep reducing the code until you can't reduce it further and also look into the chrome debugger. – Evert Nov 07 '22 at 00:53
  • That said, you are probably missing a catch at the right place. This stuff usually becomes more obvious when you convert your code from then/catch to async/await though. Also usually it's a red flag to see too many catches because if you catch, it doesn't bubble back up to the caller. – Evert Nov 07 '22 at 00:55
  • @Evert thanks. If you're talking about the dev tools built into browsers, I use those. The function calls that originate this are in the VimeoVideo.js for updateWatchedStatus. That calls either UserMediaApi.create() or.update() and the only call to catch is there in the code. The error being throw in processRestResponse() in the generic API helper class never makes it to them. My research leads me to believe I need to re-write this and wrap the fetch() in a Promise and utilize it's resolve/reject to reject rather than throw an error... more to come. – BVBAccelerate Nov 07 '22 at 15:59
  • "*I've implemented .catch() blocks in several places but I always get an error "Uncaught (in promise)" regardless of putting catch() calls everywhere.*" - these indeed *should* work. Can you show us the code of `handleErrorResponse` please? Does it re-`throw` the error? And where/how is `updateWatchedStatus` called? Also, what is the message of the uncaught error that you are getting, what is its stack trace? – Bergi Feb 06 '23 at 23:11
  • @Bergi thanks for helping. The full code is in the 3 pastebin links in the original question. updateWatchedStatus() is called as a result of someone interacting with the video. It calls create() or update(). Each of those use request() followed by a .then() using processRestResponse(). That's where the error is thrown, but the catch blocks are never triggered. handleErrorResponse() just alerts and console logs. I'm just noticing in my solution I moved processRestResponse() from create() and update() to request(). Maybe that was part of the issue. If you have any other insights please share. – BVBAccelerate Feb 07 '23 at 22:38

1 Answers1

0

Figured out and fixed this a while ago and figured I should come back in case it helps anyone.

Wrapping the request in a promise, and passing its resolve/reject into promises returned was the solution.

The code below isn't complete but it's enough to illustrate what had to be done to get this working as intended:

const request = function(resource, options){
    return new Promise((resolve, reject) => {
        return fetch(resource, options)
            .then(response => {
                if(
                    options &&
                    options.method == "DELETE" &&
                    response.status == 204
                ){
                    // --- Yii2 will return a 204 response on successful deletes and
                    // --- running response.json() on that will result in an error
                    // --- "SyntaxError: Unexpected end of JSON input" so we will just
                    // --- avoid that by returning an empty object
                    return Promise.all([response, JSON.stringify("{}"), resolve, reject])
                }
                // --- Include resolve/reject for proper error handling by response processing
                return Promise.all([response, response.json(), resolve, reject])
            }).then(this.processRestResponse)
    });
}
const create = function(postData, options){
    const url = new URL(this.baseUrl+'/'+this.resourceUrl());
    if(!options){
        options = {};
    }
    options = {
        method: 'POST',
        body: JSON.stringify(postData),
        ...options,
    }

    if(!options.headers){
        options.headers = {};
    }
    options.headers = {
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
        "Content-Type": "application/json",
        ...options.headers
    }
    return this.request(url, options);
}
const processRestResponse = function([response, body, resolve, reject]){
    // --- If the response is okay pass it all through to the function
    // --- that will be handling a response
    if(response.ok){
        return resolve([response, body]);
    }
    // --- If there are validation errors prepare them in a string
    // --- to throw a user friendly validation error message
    if(
        response.status == 422 &&
        Array.isArray(body)
    ){
        let messages = [];
        body.forEach(validationError => {
            messages.push(validationError.message);
        })
        return reject({
            name: response.status,
            message: messages.join("\n")
        })
    }
    // --- If there is another error just provide the status text
    // --- as a message (Yii provides this)
    return reject({
        name: response.status,
        message: (body.message) ?
            body.message :
            response.statusText
    })
}
export default {
    baseUrl: '',

    resourceUrl: resourceUrl,
    request: request,
    create: create,

    processRestResponse: processRestResponse,
    handleErrorResponse: handleErrorResponse
};
BVBAccelerate
  • 172
  • 1
  • 5
  • 17
  • Avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! – Bergi Feb 06 '23 at 00:20
  • @Bergi can you explain what you mean and then provide an alternate solution that solves the problem in the original question? – BVBAccelerate Feb 06 '23 at 19:28
  • The `return new Promise((resolve, reject) => {})` is the problem. It should only be used for promisifying callback-based APIs. Your original code (from the question) was fine and much better. The code from your answer ignores various errors and will lead to real uncaught promise rejections (that cannot even be caught from outside) when e.g. `fetch()` rejects with a network error. – Bergi Feb 06 '23 at 23:14