Detecting and Managing Errors in SDL3

Detecting and responding to SDL3 runtime errors, and learn about the updated error-handling patterns in the new library version.

Ryan McCombe
Updated

As with any code, interacting with SDL can sometimes result in errors. For example, let's imagine we're trying to create a window that uses Metal, which is Apple's API for creating high-performance graphics.

To do this, we can pass the SDL_WINDOW_METAL flag to SDL_CreateWindow():

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* Window{SDL_CreateWindow(
    "Hello World",
    1024, 768,
    SDL_WINDOW_METAL
  )};

  if (Window) {
    SDL_DestroyWindow(Window);
  }
  SDL_Quit();
  return 0;
}

If this program is run on a platform that doesn't support Metal (such as a Windows machine), window creation will fail, and it might not be obvious why that is.

In this lesson, we'll introduce the ways we can detect and recover from errors coming from SDL.

Retrieving Error Messages

The SDL_GetError() function is the main way we retrieve information about errors coming from SDL. It returns a string, representing the most recent error that occurred.

If we call this function after attempting to create our window, we'll get more information about what's going wrong:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* Window{SDL_CreateWindow(
    "Hello World",
    1024, 768,
    SDL_WINDOW_METAL
  )};

  std::cout << SDL_GetError();

  if (Window) {
    SDL_DestroyWindow(Window);
  }
  SDL_Quit();
  return 0;
}
Metal support is either not configured in SDL or not available in current SDL video driver (windows) or platform

SDL_GetError() can only return the most recent error message. In the following example, we have two errors - one when we create the window, as before, and a second error when we then try to get the surface associated with that (non-existent) window:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* Window{SDL_CreateWindow(
    "Hello World",
    1024, 768,
    SDL_WINDOW_METAL 
  )};

  SDL_GetWindowSurface(Window);

  std::cout << SDL_GetError();

  if (Window) {
    SDL_DestroyWindow(Window);
  }
  SDL_Quit();
  return 0;
}

When we called SDL_GetError(), the most recent error was from the SDL_GetWindowSurface() call. SDL_CreateWindow() failed, but information about that error was overwritten by the error from SDL_GetWindowSurface():

Invalid window

Additionally, SDL_GetError() does not clear the error state. Subsequent calls to SDL_GetError() will return the same message until a new error occurs:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  SDL_GetWindowSurface(nullptr);

  std::cout << SDL_GetError() << '\n';
  std::cout << SDL_GetError() << '\n';
  std::cout << SDL_GetError() << '\n';

  SDL_Quit();
  return 0;
}
Invalid window
Invalid window
Invalid window

The char* Data Type

In previous lessons, we've been using std::string when working with strings of text. SDL uses a more primitive type, sometimes called a C-style string.

Its specific type is a const char* - that is, a pointer to an individual character (char) that we cannot modify:

const char* Error = SDL_GetError();

Like any pointer, a char* represents a location in memory. In this case, it represents the first character in the string.

Subsequent characters are then stored in subsequent memory addresses, until we encounter a special character that is designed to represent the end of the string. That special character is called the null terminator and is typically represented as a backslash followed by 0: \0:

Through this simple convention, a single memory address can represent strings of any length. If the first character in a string is the null terminator \0, that is equivalent to the string being empty:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>

int main(int, char**){
  SDL_Init(SDL_INIT_VIDEO);

  const char* Error{SDL_GetError()};
  if (*Error == '\0') {
    std::cout << "There is no error";
  } else {
    std::cout << "Error: " << Error;
  }

  SDL_Quit();
  return 0;
}
There is no error

C-Style Strings and std::string

C-Style strings are primitive, and working with raw memory addresses is error-prone. Often, we'll want to use a more powerful and modern string representation, such as std::string, in our projects. This means we need to deal with conversions between std::string and char* when interacting with the SDL API.

std::string has a constructor that accepts a char*, so converting a string returned from SDL to a std::string is easy:

const char* Error{SDL_GetError()};
std::string Error{SDL_GetError()};

The std::string type also has a much friendlier API:

const char* Error{SDL_GetError()};
bool isEmpty{*Error == '\0'};

std::string Error{SDL_GetError()};
bool isEmpty{Error.empty()};

Updating our earlier code to use a std::string might look like this:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include <string>

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);

  std::string Error{SDL_GetError()};
  if (Error.empty()) {
    std::cout << "There is no error";
  } else {
    std::cout << "Error: " << Error;
  }

  SDL_Quit();
  return 0;
}
There is no error

When we have a std::string and need an equivalent char* to pass to some SDL function, the c_str() method can help us:

std::string SomeString{"Hello"};
const char* SomeCString{SomeString.c_str()};

SDL_CreateWindow() is an example of where this might be needed, as the window title needs to be provided as a const char*:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <string>

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);

  std::string GameName{"My Game"}; 
  SDL_Window* Window{SDL_CreateWindow(
    GameName.c_str(), 
    1024, 768, 0
  )};

  if (Window) {
    SDL_DestroyWindow(Window);
  }
  SDL_Quit();
  return 0;
}

We cover C-style strings and std::string in much more detail in our lesson on .

Clearing Errors

Once we've processed and acknowledged an error, we can call SDL_ClearError().

This will cause SDL_GetError() to return an empty string, unless another error has occurred between the SDL_ClearError() call and the next SDL_GetError() call:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  SDL_GetWindowSurface(nullptr);
  std::cout << "Error: " << SDL_GetError();
  SDL_ClearError();
  std::cout << "\nError: " << SDL_GetError();

  SDL_Quit();
  return 0;
}
Error: Invalid window
Error:

Creating an Error Handler

If we're going to be doing a lot of error checking, it can be helpful to create a simple function that makes this easier. The following function:

  • Accepts a string, letting the caller explain what action is associated with the error check
  • If SDL_GetError() returns a non-empty string, it logs that error, as well as the action that caused it for context
  • Clears SDL's error string using SDL_ClearError()

Files

src
Select a file to view its content

We can use it like this:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>
#include "ErrorHandling.h"

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* Window{SDL_CreateWindow(
    "Hello World",
    1024, 768,
    SDL_WINDOW_METAL 
  )};
  CheckSDLError("Creating Window");

  SDL_GetWindowSurface(Window);
  CheckSDLError("Getting Surface");

  std::cout << "Hello World\n";
  // No error has occurred since the last call
  // to CheckSDLError, so this won't log anything
  CheckSDLError("Saying Hello");

  if (Window) {
    SDL_DestroyWindow(Window);
  }
  SDL_Quit();
  return 0;
}
Creating Window Error: Metal support is either not configured in SDL or not available in current SDL video driver (windows) or platform
Getting Surface Error: Invalid window
Hello World

Toggling Error Logs at Build Time

We may want to disable calls to this function in our final released games. We can have the preprocessor include them only if some macro, such as ERROR_LOGGING, is defined.

This definition would typically be set through our build management tools but, for this example, we'll just define it in our code file:

src/ErrorHandling.h

#pragma once
#include <SDL3/SDL.h>
#include <iostream>
#include <string>

// Define me to enable error logging:
#define ERROR_LOGGING

void CheckSDLError(const std::string& Action) {
#ifdef ERROR_LOGGING
  const char* Error{SDL_GetError()};
  if (*Error != '\0') {
    std::cout << Action << " Error: "
      << Error << '\n';
    SDL_ClearError();
  }
#endif
}

Detecting Specific Errors

In most advanced programs, our error handling will often want to go beyond simply logging out what went wrong. We'll often want to implement logic that reacts and potentially recovers from certain types of errors.

In addition to updating the string returned by SDL_GetError(), SDL functions will often use their return values to indicate something went wrong.

We need to refer to the official documentation for information on any specific function. However, as a general pattern, functions that return a pointer, such as SDL_CreateWindow(), will return a nullptr if something went wrong:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* Window{SDL_CreateWindow(
    "Hello World",
    1024, 768,
    SDL_WINDOW_METAL 
  )};

  if (!Window) {
    std::cout << "Couldn't create window -"
                 " trying without Metal\n";

    Window = SDL_CreateWindow(
      "Hello World",
      1024, 768,
      0
    );
  }

  if (Window) {
    std::cout << "Window created successfully\n";
    SDL_ClearError();
    SDL_DestroyWindow(Window);
  }

  SDL_Quit();
  return 0;
}
Couldn't create window - trying without Metal
Window created successfully

Other functions that can fail will typically return a boolean value. For example, SDL_Init() will return true if it was successful, and false otherwise:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>

int main(int, char**) {
  if (!SDL_Init(SDL_INIT_VIDEO)) {
    std::cout << "Init failed: "
              << SDL_GetError();
  }

  SDL_Quit();
  return 0;
}

Setting Custom Errors

While SDL_SetError() is primarily used internally by SDL functions, we can also use it in our own code if we want to use SDL's error reporting system.

This can be useful when we want to use a consistent error-handling mechanism throughout our project, even for non-SDL related errors:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <iostream>

int Divide(int a, int b) {
  if (b == 0) {
    SDL_SetError("Cannot divide by 0");
    return 0;
  }
  return a / b;
}

int main(int, char**) {
  SDL_Init(SDL_INIT_VIDEO);
  Divide(2, 0);
  std::cout << SDL_GetError();
  // Alternatively, using the function
  // we created earlier:
  // CheckSDLError("Dividing");

  SDL_Quit();
  return 0;
}
Cannot divide by 0

Summary

In this lesson, we've explored techniques for handling errors when working with SDL3. We learned:

  • How to use SDL_GetError() to retrieve human-readable error messages.
  • An introduction to working with C-style strings (char*) and how they interact with std::string.
  • The use of SDL_ClearError() to reset the internal error state.
  • How to implement a custom error handling function to streamline error checking.
  • How to detect specific errors by examining the return values from SDL functions, such as nullptr for pointers and false for functions that return a bool.
  • How SDL_SetError() can be used to integrate our custom errors into SDL's system.
Next Lesson
Lesson 19 of 25

Implementing an Application Loop

Step-by-step guide on creating the SDL3 application and event loops.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate