Error Handling in the After Effects SDK: A Beginner’s Guide

Sample code in the After Effects SDK follows an elegant fail fast method consisting of an error code enumeration and two error handling macros. It can be a little confusing at first but once you get the hang of it, you’ll really appreciate the simplicity and maintainability of it. Let’s dive into how it works.

// defined in AE_Effect.h
enum {
    PF_Err_NONE = 0,
    PF_Err_OUT_OF_MEMORY = 4,
    PF_Err_INTERNAL_STRUCT_DAMAGED = PF_FIRST_ERR,
    PF_Err_INVALID_INDEX,
    PF_Err_UNRECOGNIZED_PARAM_TYPE,
    PF_Err_INVALID_CALLBACK,
    PF_Err_BAD_CALLBACK_PARAM,
    PF_Interrupt_CANCEL,
    PF_Err_CANNOT_PARSE_KEYFRAME_TEXT
};
typedef	A_long PF_Err;

Two ERR macros (to wrap them all)

// defined in AE_Macros.h
#ifndef ERR
    #define ERR(FUNC)	do { if (!err) { err = (FUNC); } } while (0)
#endif

#ifndef ERR2
    #define ERR2(FUNC)	do { if (((err2 = (FUNC)) != A_Err_NONE) && !err) err = err2; } while (0)
#endif

Let’s rearrange that

The above macros are likely laid out for brevity, but it can help to rewrite them with added formatting to understand what’s going on. Don’t worry too much about the finer details, we’ll get to that eventually. The ERR2 macro is a little confusing, but all you need to know at the start is that any deallocation should be wrapped in this code; just because we have an upstream error code doesn’t mean we don’t need to clean up after ourselves!

#ifndef ERR
    // ERR macro: Executes FUNC only if 'err' is currently 0, then stores its return value in 'err'
    #define ERR(FUNC)                           \
        do {                                    \
            if (!err) {                         \
                err = (FUNC);                   \
            }                                   \
        } while (0)
#endif

#ifndef ERR2
    // ERR2 macro: ALWAYS Executes FUNC, stores result in 'err2', and if the result
    // is non-zero AND 'err' is currently 0, then copies err2 to err
    #define ERR2(FUNC)                          \
        do {                                    \
            if (((err2 = (FUNC)) != A_Err_NONE) && !err) {    \
                err = err2;                     \
            }                                   \
        } while (0)
#endif

Our macros in practice

All non-void API calls return error codes. You can assign the codes directly, but it’s recommended to wrap calls inside a specific ERR macro. When all API calls are wrapped inside error macros, the following behaviour is enforced:

  • As soon as one API call reports an error, no other calls are executed (with the exception of deallocation calls wrapped inside the err2 macro)

  • The original error code is preserved. This is very important for finding and debugging the cause of the failure, rather than any symptoms that occur as a result of the initial failure

Example code from Convolutrix.cpp

Take a look at the following chain of API calls from the Convolutrix sample. The flow is as follows:

  1. Allocate a buffer of pixels

  2. Fill that buffer with our custom colour

  3. Blend that filled buffer into the output buffer

  4. Clean up the allocation, checking that the data pointer is valid beforehand

So what’s happening under the hood with our error macros? Let’s assume that PF_New_World failed to allocate. It’s wrapped in the ERR macro, so our err variable is now PF_Err_OUT_OF_MEMORY.

This causes all our downstream API calls to not execute since our err code is no longer 0, and just as well, as these calls would likely crash if they tried to execute on a null buffer.

Equally important is that we then check if temp.data is valid, because if we failed to allocate the buffer then we don’t need to clean it up. If we don’t do this check, the err2 macro will proceed with cleaning up a null buffer and we’ll get an error message or worse.

// create and zero out err vars
PF_Err err  = PF_Err_NONE,
       err2 = PF_Err_NONE;

PF_EffectWorld temp;
AEFX_CLR_STRUCT(temp);

// Allocate a world full of the color to blend.
ERR(PF_NEW_WORLD(output->width,
                 output->height,
                 PF_NewWorldFlag_NONE,
                 &temp));

ERR(PF_FILL(&params[CONVO_COLOR]->u.cd.value, 
            NULL, 
            &temp));

ERR(PF_BLEND(output,
            &temp,
            FLOAT2FIX((params[CONVO_BLEND_COLOR_AMOUNT]->u.fs_d.value / 100.0)),
            output));

if (temp.data){ // if there's a data pointer, we allocated something
    ERR2(PF_DISPOSE_WORLD(&temp));
}

Any API call can (and will) fail… eventually

Now consider a different case: the PF_NEW_WORLD allocation succeeds but we receive an error during PF_FILL. This could happen for a myriad of reasons, but the most likely case if an allocation succeeded but a downstream call errors is that the user pressed escape during the execution and we now have the error code PF_Interrupt_CANCEL. In this case, PF_BLEND skips due to the non-zero error code. Temp.data is valid, so we still execute the PF_DISPOSE_WORLD call.

Remember all the extra code in the err2 macro? This is essentially deciding what to do if the allocation function reports an error code: if we have a previous error code, disregard the deallocation error because we don’t want to overwrite the PF_FILL error code. If we didn’t have a previous error code, then and only then do we assign the deallocation error to the err variable.

At the end of the function, we only ever return the single error code. By having two separate err variables (err and err2), we can prioritise the initial error code. To see how much value we’re getting out of these macros, just look at an alternative version of the code:

PF_NEW_WORLD(output->width, output->height, PF_NewWorldFlag_NONE, &temp);

if(temp.data)
{
    err = PF_FILL(&params[CONVO_COLOR]->u.cd.value, NULL, &temp);
    
    if(!err)
    {
        err = PF_BLEND(output, &temp, FLOAT2FIX((params[CONVO_BLEND_COLOR_AMOUNT]->u.fs_d.value / 100.0)), output);
    
        if(!err)
        {
            // each additional callback requiring an explicit check
        }
    }
    
    err2 = PF_DISPOSE_WORLD(&temp);
    
    if(!err && err2)
    {
        err = err2;
    }
}

You could end up with an indentation for each API call. It’s not uncommon to have 5-10 of these calls in a row so it’s best to avoid this wherever we can!

A high level overview of nested calls

When is our PF_Err code finally returned to the host? Once per command selector, inside the main entry point’s try, catch, then switch:

PF_Err OurRenderFunc()
{
    PF_Err	err	= 	PF_Err_NONE,	
                err2	=	PF_Err_NONE;

    // chain of API calls wrapped inside the ERR macro here

    return err; // single point of return, returning only the first code
}

// main entry point also returns PF_Err
PF_Err
EffectMain(
    PF_Cmd			cmd,
    PF_InData			*in_data,
    PF_OutData			*out_data,
    PF_ParamDef			*params[],
    PF_LayerDef			*output,
    void			*extra)
{
    PF_Err err = PF_Err_NONE; // only one err variable here
    
    try 
    {
        switch (cmd) 
        {
            case PF_Cmd_SMART_RENDER:
                err = OurRenderFunc();
                break;
            // ...other cases inside this switch statement
        }
    } catch (PF_Err &thrown_err) {
        err = thrown_err;
    }

    return err; // entry point returns our err code to the host
}

What does After Effects do with our returned PF_Err codes?

After Effects responds according to the PF_Err codes we return. If we return PF_Err_INVALID_CALLBACK in a render call, that particular render will be considered to have failed. AE will still send calls for other frames to be rendered by the plugin. Contrast this if we return PF_Err_INTERNAL_STRUCT_DAMAGED, it’s assumed that a vital struct or dependency needed by all instances of the plugin is damaged and therefore no further calls will be sent to any instances of the plugin, effectively stopping it from working until the host is restarted.

AE out of memory? Never happened!

Probably the most common error code you’ll receive is PF_Err_OUT_OF_MEMORY, which presumably can be returned by any API call that allocates memory. Keep in mind this isn’t necessarily a big deal when in an MFR environment; AE may just have been overzealous with its threading goals and will try the same render call again with less concurrency.

On the other hand, if you try to allocate a handle that’s larger than AE’s internal limits (to my knowledge, unchanged at 2GB), then AE will pop a user facing error and this will fail a render if in the queue.

Mystery Codes

I’d love to know the finer details of the intended differences of the other error codes though there’s little documentation on them. We just have to guess from the context they are used in in the sample code.

  • PF_Err_INVALID_INDEX (not used in any of the AESDK samples)

  • PF_Err_UNRECOGNIZED_PARAM_TYPE

  • PF_Err_BAD_CALLBACK_PARAM

PF_Err_UNRECOGNIZED_PARAM_TYPE is used in the samples when an argument isn’t of the expected type, refcon, or is null. But so is PF_Err_BAD_CALLBACK_PARAM, so I’m not sure the intended difference between them or how the host will react differently.

// SDK_Noise.cpp


if (destinationPixelFormat == PrPixelFormat_BGRA_4444_8u)
{
    ...
} else if (destinationPixelFormat == PrPixelFormat_VUYA_4444_8u){
    ...
} else if (destinationPixelFormat == PrPixelFormat_BGRA_4444_32f) {
    ...
} else if (destinationPixelFormat == PrPixelFormat_VUYA_4444_32f) {
    ...
} else {
    //	Return error, because we don't know how to handle the specified pixel type
    return PF_Err_UNRECOGNIZED_PARAM_TYPE;
}
// Smartypants.cpp

switch (format) {
    case PF_PixelFormat_ARGB128:
        ERR(suites.IterateFloatSuite2()->iterate_origin(...)
        break;
    case PF_PixelFormat_ARGB64:
        ERR(suites.Iterate16Suite2()->iterate_origin(...)
        break;
    case PF_PixelFormat_ARGB32:
        ERR(suites.Iterate8Suite2()->iterate_origin(...)
        break;
    default:
        err = PF_Err_BAD_CALLBACK_PARAM;
        break;
}

What else don’t we know?

Something the SDK is short on is documentation regarding which API calls return which codes. For example, which calls can return a PF_Interrupt_CANCEL code? Is it possible that in the <1ms the host took to retrieve a layerH the user just happened to press the escape key and therefore it can return the interrupt code? I have no idea!


In a future post we’ll cover a few other related topics:

  • Alternative macros and methods to fail fast (fail immediately)

  • Common mistakes beginners make

  • One of our error codes is not like the other